@jefuriiij/synthra 0.1.16 → 0.1.18
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 +41 -0
- package/dist/cli/index.js +68 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +52 -13
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ For older versions, see [GitHub Releases](https://github.com/jefuriiij/synthra/r
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [0.1.18] — 2026-06-01
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Stop hook on Linux/macOS no longer posts zero tokens to the dashboard.** The bash
|
|
15
|
+
`stop.sh` hook extracted `transcript_path` from the Claude Code Stop payload using a
|
|
16
|
+
greedy `sed` capture (`\(.*\)"`). Because the real payload has additional fields after
|
|
17
|
+
`transcript_path`, the capture grabbed those trailing fields and produced a
|
|
18
|
+
non-existent path string. The `-f` file check therefore always failed, totals were
|
|
19
|
+
never POSTed to `/log`, and the dashboard stayed stuck at 0 on every turn (GitHub
|
|
20
|
+
issue #1). Fixed by parsing with `jq -r '.transcript_path // empty'` and moving the
|
|
21
|
+
`command -v jq` guard above the parse so the hook exits cleanly when `jq` is absent.
|
|
22
|
+
- **SessionStart/PreCompact primer hook (`prime.sh`) hardened the same way.** The
|
|
23
|
+
`/prime` response is `{"primer":"…","port":…}`, so the old greedy capture accidentally
|
|
24
|
+
injected trailing `","port":…` junk into the primer string. Because primer text can
|
|
25
|
+
contain inner quotes, a negated-class fix (`[^"]*`) would have truncated it at the
|
|
26
|
+
first quote — `jq -r '.primer // empty'` is the correct parse. Switched `printf '%b'`
|
|
27
|
+
to `printf '%s'` since `jq -r` already decodes JSON escapes.
|
|
28
|
+
- Both fixes are **bash-only**. The Windows PowerShell hooks (`stop.ps1`, `prime.ps1`)
|
|
29
|
+
use `ConvertFrom-Json` and were already correct.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## [0.1.17] — 2026-05-29
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`syn .` scaffolds an agent-onboarding CLAUDE.md on brand-new projects.**
|
|
38
|
+
When a project has no CLAUDE.md, Synthra now writes a lean skeleton —
|
|
39
|
+
`Build & test`, `Conventions`, `Key decisions`, `Gotchas` (with TODO
|
|
40
|
+
prompts) — *above* its managed policy block, instead of a bare policy
|
|
41
|
+
block. This is the durable "why/how" layer the graph can't infer; the
|
|
42
|
+
graph still owns "what/where." Fill it in, or run `/init` to auto-draft.
|
|
43
|
+
The skeleton is written **once** and lives outside the
|
|
44
|
+
`<!-- synthra-policy -->` markers, so re-running `syn .` (which
|
|
45
|
+
refreshes the policy block) never clobbers what you've written.
|
|
46
|
+
Projects that already have a CLAUDE.md are untouched — no skeleton is
|
|
47
|
+
injected.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
10
51
|
## [0.1.16] — 2026-05-29
|
|
11
52
|
|
|
12
53
|
### Changed
|
package/dist/cli/index.js
CHANGED
|
@@ -18,7 +18,7 @@ var init_package = __esm({
|
|
|
18
18
|
"package.json"() {
|
|
19
19
|
package_default = {
|
|
20
20
|
name: "@jefuriiij/synthra",
|
|
21
|
-
version: "0.1.
|
|
21
|
+
version: "0.1.18",
|
|
22
22
|
publishConfig: {
|
|
23
23
|
access: "public"
|
|
24
24
|
},
|
|
@@ -1466,12 +1466,18 @@ if [ ! -f "$PORT_FILE" ]; then exit 0; fi
|
|
|
1466
1466
|
PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
1467
1467
|
if [ -z "$PORT" ]; then exit 0; fi
|
|
1468
1468
|
|
|
1469
|
+
# Parse the primer with jq, not sed. Primer text legitimately contains quotes, so a
|
|
1470
|
+
# negated-class capture ([^"]*) would truncate it at the first inner quote, while the
|
|
1471
|
+
# old greedy capture (.*") over-ran into the trailing "port" field and injected junk.
|
|
1472
|
+
# jq -r also decodes JSON escapes, so we print with %s (not %b). No jq \u2192 no primer.
|
|
1473
|
+
if ! command -v jq >/dev/null 2>&1; then exit 0; fi
|
|
1474
|
+
|
|
1469
1475
|
PRIMER=$(curl -sS --max-time 3 "http://127.0.0.1:$PORT/prime" 2>/dev/null \\
|
|
1470
|
-
|
|
|
1476
|
+
| jq -r '.primer // empty' 2>/dev/null \\
|
|
1471
1477
|
| head -c 8000)
|
|
1472
1478
|
|
|
1473
1479
|
if [ -n "$PRIMER" ]; then
|
|
1474
|
-
printf '%
|
|
1480
|
+
printf '%s\\n' "$PRIMER"
|
|
1475
1481
|
fi
|
|
1476
1482
|
exit 0
|
|
1477
1483
|
`;
|
|
@@ -1564,7 +1570,13 @@ set +e\r
|
|
|
1564
1570
|
INPUT=$(cat 2>/dev/null)\r
|
|
1565
1571
|
if [ -z "$INPUT" ]; then exit 0; fi\r
|
|
1566
1572
|
\r
|
|
1567
|
-
|
|
1573
|
+
# jq is required for the parsing below \u2014 bail early (silent no-op) if it's absent.\r
|
|
1574
|
+
if ! command -v jq >/dev/null 2>&1; then exit 0; fi\r
|
|
1575
|
+
\r
|
|
1576
|
+
# Extract transcript_path with jq, not sed. A greedy sed capture (\\(.*\\)") grabs the\r
|
|
1577
|
+
# trailing JSON fields after transcript_path and yields a path that doesn't exist, so\r
|
|
1578
|
+
# the -f check below always failed and totals were never POSTed to /log. (issue #1)\r
|
|
1579
|
+
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)\r
|
|
1568
1580
|
if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then exit 0; fi\r
|
|
1569
1581
|
\r
|
|
1570
1582
|
PORT_FILE="$PWD/.synthra-graph/mcp_port"\r
|
|
@@ -1572,8 +1584,6 @@ if [ ! -f "$PORT_FILE" ]; then exit 0; fi\r
|
|
|
1572
1584
|
PORT=$(cat "$PORT_FILE" 2>/dev/null | tr -d '[:space:]')\r
|
|
1573
1585
|
if [ -z "$PORT" ]; then exit 0; fi\r
|
|
1574
1586
|
\r
|
|
1575
|
-
if ! command -v jq >/dev/null 2>&1; then exit 0; fi\r
|
|
1576
|
-
\r
|
|
1577
1587
|
OFFSET_FILE="\${TRANSCRIPT}.stopoffset"\r
|
|
1578
1588
|
START_OFFSET=0\r
|
|
1579
1589
|
if [ -f "$OFFSET_FILE" ]; then\r
|
|
@@ -3198,9 +3208,11 @@ async function readSymbolIndex(path) {
|
|
|
3198
3208
|
|
|
3199
3209
|
// src/cli/bootstrap.ts
|
|
3200
3210
|
import { mkdir as mkdir5, readFile as readFile10, stat as stat2, writeFile as writeFile5 } from "fs/promises";
|
|
3211
|
+
import { basename as basename4 } from "path";
|
|
3201
3212
|
|
|
3202
3213
|
// src/hooks/claude-md.ts
|
|
3203
3214
|
import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
|
|
3215
|
+
import { basename as basename3, dirname as dirname6 } from "path";
|
|
3204
3216
|
var POLICY_VERSION = 3;
|
|
3205
3217
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
3206
3218
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
@@ -3301,7 +3313,39 @@ function policyBlock() {
|
|
|
3301
3313
|
POLICY_END
|
|
3302
3314
|
].join("\n");
|
|
3303
3315
|
}
|
|
3304
|
-
|
|
3316
|
+
function onboardingSkeleton(projectName) {
|
|
3317
|
+
return [
|
|
3318
|
+
`# ${projectName}`,
|
|
3319
|
+
"",
|
|
3320
|
+
"> Onboarding notes for AI coding agents. Synthra's graph already knows the",
|
|
3321
|
+
"> code's *structure* (files, symbols, imports) \u2014 this file is for what the",
|
|
3322
|
+
"> graph can't infer: how to run the project, its conventions, and the",
|
|
3323
|
+
"> decisions behind them. Keep it lean and current; delete prompts you don't need.",
|
|
3324
|
+
"",
|
|
3325
|
+
"## Build & test",
|
|
3326
|
+
"",
|
|
3327
|
+
"- TODO: install deps / build",
|
|
3328
|
+
"- TODO: run tests / lint / typecheck",
|
|
3329
|
+
"- TODO: run the app locally",
|
|
3330
|
+
"",
|
|
3331
|
+
"## Conventions",
|
|
3332
|
+
"",
|
|
3333
|
+
"- TODO: code style, naming, file layout the agent should follow",
|
|
3334
|
+
"",
|
|
3335
|
+
"## Key decisions",
|
|
3336
|
+
"",
|
|
3337
|
+
'- TODO: non-obvious choices and *why* ("we use X not Y because \u2026")',
|
|
3338
|
+
"",
|
|
3339
|
+
"## Gotchas",
|
|
3340
|
+
"",
|
|
3341
|
+
`- TODO: traps, footguns, "don't touch X without Y"`,
|
|
3342
|
+
"",
|
|
3343
|
+
"_Tip: run `/init` in Claude Code to auto-draft the sections above, then trim",
|
|
3344
|
+
"to the durable bits. Synthra manages its own block below \u2014 leave it._",
|
|
3345
|
+
""
|
|
3346
|
+
].join("\n");
|
|
3347
|
+
}
|
|
3348
|
+
async function patchClaudeMd(path, projectName) {
|
|
3305
3349
|
let existing;
|
|
3306
3350
|
try {
|
|
3307
3351
|
existing = await readFile9(path, "utf8");
|
|
@@ -3310,7 +3354,8 @@ async function patchClaudeMd(path) {
|
|
|
3310
3354
|
}
|
|
3311
3355
|
const block = policyBlock();
|
|
3312
3356
|
if (existing === null) {
|
|
3313
|
-
|
|
3357
|
+
const name = projectName || basename3(dirname6(path)) || "this project";
|
|
3358
|
+
await writeFile4(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
|
|
3314
3359
|
return { created: true, updated: false, skipped: false };
|
|
3315
3360
|
}
|
|
3316
3361
|
const stripped = existing.replace(ANY_BLOCK_RE, "");
|
|
@@ -3367,7 +3412,7 @@ async function bootstrap(paths) {
|
|
|
3367
3412
|
const contextCreated = await ensureDir(paths.contextDir);
|
|
3368
3413
|
const gitignoreUpdated = await patchGitignore(paths.gitignore);
|
|
3369
3414
|
const claudeMdExistedBefore = await exists(paths.claudeMd);
|
|
3370
|
-
const patch = await patchClaudeMd(paths.claudeMd);
|
|
3415
|
+
const patch = await patchClaudeMd(paths.claudeMd, basename4(paths.projectRoot));
|
|
3371
3416
|
return {
|
|
3372
3417
|
graphCreated,
|
|
3373
3418
|
contextCreated,
|
|
@@ -3420,8 +3465,12 @@ async function scanProject(projectRootRaw, opts = {}) {
|
|
|
3420
3465
|
if (boot.graphCreated) log.info(" created .synthra-graph/");
|
|
3421
3466
|
if (boot.contextCreated) log.info(" created .synthra/");
|
|
3422
3467
|
if (boot.gitignoreUpdated) log.info(" updated .gitignore");
|
|
3423
|
-
if (boot.claudeMdCreated)
|
|
3424
|
-
|
|
3468
|
+
if (boot.claudeMdCreated) {
|
|
3469
|
+
log.info(" created CLAUDE.md \u2014 onboarding skeleton for the agent");
|
|
3470
|
+
log.info(" \u21B3 fill in Build / Conventions / Decisions (or run /init in Claude to auto-draft)");
|
|
3471
|
+
} else if (boot.claudeMdUpdated) {
|
|
3472
|
+
log.info(" updated CLAUDE.md");
|
|
3473
|
+
}
|
|
3425
3474
|
}
|
|
3426
3475
|
const walked = [];
|
|
3427
3476
|
for await (const file of walk(projectRoot)) walked.push(file);
|
|
@@ -3721,7 +3770,7 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
|
|
|
3721
3770
|
|
|
3722
3771
|
// src/memory/context-md.ts
|
|
3723
3772
|
import { mkdir as mkdir6, readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
3724
|
-
import { dirname as
|
|
3773
|
+
import { dirname as dirname7 } from "path";
|
|
3725
3774
|
var MAX_BULLETS = 3;
|
|
3726
3775
|
function deriveContextMd(entries, branch) {
|
|
3727
3776
|
const tasks = entries.filter((e) => e.type === "task").reverse();
|
|
@@ -3764,13 +3813,13 @@ function formatContextMd(ctx) {
|
|
|
3764
3813
|
return lines.join("\n");
|
|
3765
3814
|
}
|
|
3766
3815
|
async function writeContextMd(path, ctx) {
|
|
3767
|
-
await mkdir6(
|
|
3816
|
+
await mkdir6(dirname7(path), { recursive: true });
|
|
3768
3817
|
await writeFile6(path, formatContextMd(ctx), "utf8");
|
|
3769
3818
|
}
|
|
3770
3819
|
|
|
3771
3820
|
// src/memory/context-store.ts
|
|
3772
3821
|
import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
|
|
3773
|
-
import { dirname as
|
|
3822
|
+
import { dirname as dirname8 } from "path";
|
|
3774
3823
|
var SCHEMA_VERSION2 = 1;
|
|
3775
3824
|
async function readEntries(path) {
|
|
3776
3825
|
try {
|
|
@@ -3782,7 +3831,7 @@ async function readEntries(path) {
|
|
|
3782
3831
|
}
|
|
3783
3832
|
}
|
|
3784
3833
|
async function writeEntries(path, entries) {
|
|
3785
|
-
await mkdir7(
|
|
3834
|
+
await mkdir7(dirname8(path), { recursive: true });
|
|
3786
3835
|
const store = { schema_version: SCHEMA_VERSION2, entries };
|
|
3787
3836
|
await writeFile7(path, JSON.stringify(store, null, 2) + "\n", "utf8");
|
|
3788
3837
|
}
|
|
@@ -4477,7 +4526,7 @@ async function handleContextUpdate(req, ctx) {
|
|
|
4477
4526
|
|
|
4478
4527
|
// src/server/routes/gate.ts
|
|
4479
4528
|
import { appendFile as appendFile2, mkdir as mkdir8 } from "fs/promises";
|
|
4480
|
-
import { dirname as
|
|
4529
|
+
import { dirname as dirname9 } from "path";
|
|
4481
4530
|
var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
|
|
4482
4531
|
var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
|
|
4483
4532
|
function extractQuery(toolName, input) {
|
|
@@ -4507,7 +4556,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens) {
|
|
|
4507
4556
|
}
|
|
4508
4557
|
async function logDecision(ctx, toolName, query, decision, reason) {
|
|
4509
4558
|
try {
|
|
4510
|
-
await mkdir8(
|
|
4559
|
+
await mkdir8(dirname9(ctx.paths.gateLog), { recursive: true });
|
|
4511
4560
|
const entry = {
|
|
4512
4561
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4513
4562
|
tool: toolName,
|
|
@@ -4564,14 +4613,14 @@ async function handleGate(req, ctx) {
|
|
|
4564
4613
|
|
|
4565
4614
|
// src/server/routes/log.ts
|
|
4566
4615
|
import { appendFile as appendFile3, mkdir as mkdir9 } from "fs/promises";
|
|
4567
|
-
import { dirname as
|
|
4616
|
+
import { dirname as dirname10 } from "path";
|
|
4568
4617
|
async function handleLog(entry, ctx) {
|
|
4569
4618
|
if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
|
|
4570
4619
|
throw new Error("log: input_tokens and output_tokens (number) are required");
|
|
4571
4620
|
}
|
|
4572
4621
|
const written_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
4573
4622
|
const record = { ...entry, written_at };
|
|
4574
|
-
await mkdir9(
|
|
4623
|
+
await mkdir9(dirname10(ctx.paths.tokenLog), { recursive: true });
|
|
4575
4624
|
await appendFile3(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
|
|
4576
4625
|
return { ok: true, written_at };
|
|
4577
4626
|
}
|