@ramarivera/coding-buddy 0.9.2 → 0.9.4
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +3 -1
- package/scripts/sync-version.ts +122 -0
- package/server/index.ts +4 -1
- package/server/state.ts +5 -0
- package/statusline/buddy-status.sh +31 -3
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi (requires bun: https://bun.sh/install)",
|
|
8
|
-
"version": "0.
|
|
8
|
+
"version": "0.9.4"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "coding-buddy",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi. MCP-based terminal pet with ASCII art, stats, reactions, and personality. Requires bun runtime (https://bun.sh/install).",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.9.4",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "ramarivera"
|
|
18
18
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramarivera/coding-buddy",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Persistent coding companion for Claude Code, Pi, and Oh My Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"omp": {
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"test": "bun test",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"gen:emoji-widths": "bun run scripts/gen-emoji-widths.ts",
|
|
39
|
+
"sync-version": "bun run scripts/sync-version.ts",
|
|
40
|
+
"version": "bun run sync-version && git add .claude-plugin",
|
|
39
41
|
"ci:statusline": "bash scripts/ci/check-statusline.sh",
|
|
40
42
|
"ci:vhs:render": "bash scripts/ci/render-vhs-goldens.sh",
|
|
41
43
|
"ci:vhs:update": "bash scripts/ci/render-vhs-goldens.sh --update",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync every version field in the repo to package.json.
|
|
3
|
+
*
|
|
4
|
+
* bun run sync-version # rewrite drifted fields
|
|
5
|
+
* bun run sync-version --check # exit 1 on drift, change nothing (CI)
|
|
6
|
+
*
|
|
7
|
+
* Wired into the npm `version` lifecycle so `npm version <bump>` updates the
|
|
8
|
+
* manifests in the same commit as package.json, and into CI via --check so
|
|
9
|
+
* drift cannot land.
|
|
10
|
+
*
|
|
11
|
+
* Design and the original implementation come from #110 by @grlee. That PR
|
|
12
|
+
* conflicted with main and lives on a fork, so it is reimplemented here rather
|
|
13
|
+
* than rebased.
|
|
14
|
+
*
|
|
15
|
+
* server/index.ts is deliberately NOT a target — it imports pkg.version
|
|
16
|
+
* directly, which is strictly better than syncing a literal. Anything that can
|
|
17
|
+
* import package.json should do that instead of being added below.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
21
|
+
import { join } from "path";
|
|
22
|
+
|
|
23
|
+
const ROOT = join(import.meta.dir, "..");
|
|
24
|
+
const CHECK_ONLY = process.argv.includes("--check");
|
|
25
|
+
|
|
26
|
+
const version: unknown = JSON.parse(
|
|
27
|
+
readFileSync(join(ROOT, "package.json"), "utf8"),
|
|
28
|
+
).version;
|
|
29
|
+
|
|
30
|
+
if (typeof version !== "string" || version === "") {
|
|
31
|
+
console.error("sync-version: package.json is missing a version field");
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Target {
|
|
36
|
+
/** Repo-relative path. */
|
|
37
|
+
path: string;
|
|
38
|
+
/** Every version field in the document, for reporting and for rewriting. */
|
|
39
|
+
fields: (doc: any) => { label: string; get: () => string; set: () => void }[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TARGETS: Target[] = [
|
|
43
|
+
{
|
|
44
|
+
path: ".claude-plugin/plugin.json",
|
|
45
|
+
fields: (doc) => [
|
|
46
|
+
{ label: "version", get: () => doc.version, set: () => { doc.version = version; } },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
path: ".claude-plugin/marketplace.json",
|
|
51
|
+
fields: (doc) => [
|
|
52
|
+
{
|
|
53
|
+
label: "metadata.version",
|
|
54
|
+
get: () => doc.metadata?.version,
|
|
55
|
+
// Creates metadata if absent. Guarding the write instead would let
|
|
56
|
+
// --check report drift that the fixer could never resolve.
|
|
57
|
+
set: () => {
|
|
58
|
+
doc.metadata ??= {};
|
|
59
|
+
doc.metadata.version = version;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
...(doc.plugins ?? []).map((plugin: any, i: number) => ({
|
|
63
|
+
label: `plugins[${i}].version`,
|
|
64
|
+
get: () => plugin.version,
|
|
65
|
+
set: () => { plugin.version = version; },
|
|
66
|
+
})),
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export interface Drift {
|
|
72
|
+
path: string;
|
|
73
|
+
label: string;
|
|
74
|
+
current: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Returns every field that disagrees with package.json. Never writes. */
|
|
78
|
+
export function findDrift(): Drift[] {
|
|
79
|
+
const drift: Drift[] = [];
|
|
80
|
+
for (const target of TARGETS) {
|
|
81
|
+
const doc = JSON.parse(readFileSync(join(ROOT, target.path), "utf8"));
|
|
82
|
+
for (const field of target.fields(doc)) {
|
|
83
|
+
const current = field.get();
|
|
84
|
+
if (current !== version) drift.push({ path: target.path, label: field.label, current });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return drift;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main(): void {
|
|
91
|
+
const drift = findDrift();
|
|
92
|
+
|
|
93
|
+
for (const d of drift) {
|
|
94
|
+
console.log(
|
|
95
|
+
`${CHECK_ONLY ? "DRIFT" : "sync"}: ${d.path} :: ${d.label} ${d.current} -> ${version}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (drift.length === 0) {
|
|
100
|
+
console.log(`sync-version: all version fields already at ${version}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (CHECK_ONLY) {
|
|
105
|
+
console.error(
|
|
106
|
+
"sync-version: version fields are out of sync with package.json. Run `bun run sync-version`.",
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Rewrite only the files that actually drifted, preserving 4-space style.
|
|
112
|
+
const drifted = new Set(drift.map((d) => d.path));
|
|
113
|
+
for (const target of TARGETS) {
|
|
114
|
+
if (!drifted.has(target.path)) continue;
|
|
115
|
+
const full = join(ROOT, target.path);
|
|
116
|
+
const doc = JSON.parse(readFileSync(full, "utf8"));
|
|
117
|
+
for (const field of target.fields(doc)) field.set();
|
|
118
|
+
writeFileSync(full, `${JSON.stringify(doc, null, 4)}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (import.meta.main) main();
|
package/server/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { existsSync } from "fs";
|
|
13
13
|
import { join, resolve, dirname } from "path";
|
|
14
|
+
// The version advertised in the MCP handshake. Imported rather than written
|
|
15
|
+
// as a literal so it cannot drift from the published package (it had, to 0.6.0).
|
|
16
|
+
import pkg from "../package.json" with { type: "json" };
|
|
14
17
|
|
|
15
18
|
import { generateBones,
|
|
16
19
|
generatePersonality,
|
|
@@ -125,7 +128,7 @@ function getInstructions(): string {
|
|
|
125
128
|
const server = new McpServer(
|
|
126
129
|
{
|
|
127
130
|
name: "coding-buddy",
|
|
128
|
-
version:
|
|
131
|
+
version: pkg.version,
|
|
129
132
|
},
|
|
130
133
|
{
|
|
131
134
|
instructions: getInstructions(),
|
package/server/state.ts
CHANGED
|
@@ -454,6 +454,10 @@ export interface StatusState {
|
|
|
454
454
|
reaction: string;
|
|
455
455
|
muted: boolean;
|
|
456
456
|
achievement: string;
|
|
457
|
+
/** Epoch ms the achievement was awarded; 0 when none. The statusline expires
|
|
458
|
+
* the trophy banner on this, otherwise it latches into the shared
|
|
459
|
+
* status.json and pins every session's bubble to the last unlock. */
|
|
460
|
+
achievementAt: number;
|
|
457
461
|
frames: string[];
|
|
458
462
|
compactFrames: string[];
|
|
459
463
|
minimalFrames: string[];
|
|
@@ -543,6 +547,7 @@ export function writeStatusState(
|
|
|
543
547
|
reaction: "",
|
|
544
548
|
muted: muted ?? false,
|
|
545
549
|
achievement: achievement ?? "",
|
|
550
|
+
achievementAt: achievement ? Date.now() : 0,
|
|
546
551
|
frames,
|
|
547
552
|
compactFrames,
|
|
548
553
|
minimalFrames,
|
|
@@ -58,6 +58,9 @@ STARS=$(jq -r '.stars // ""' "$STATE" 2>/dev/null)
|
|
|
58
58
|
SHINY=$(jq -r '.shiny // false' "$STATE" 2>/dev/null)
|
|
59
59
|
REACTION_FILE="$BUDDY_STATE_DIR/reaction.$SID.json"
|
|
60
60
|
ACHIEVEMENT=$(jq -r '.achievement // ""' "$STATE" 2>/dev/null)
|
|
61
|
+
# "absent" distinguishes a legacy status.json (no field at all) from an explicit
|
|
62
|
+
# 0, which means "no achievement pending" and must not render.
|
|
63
|
+
ACHIEVEMENT_AT=$(jq -r 'if has("achievementAt") then (.achievementAt // 0) else "absent" end' "$STATE" 2>/dev/null)
|
|
61
64
|
LEVEL=$(jq -r '.level // 1' "$STATE" 2>/dev/null)
|
|
62
65
|
MOOD=$(jq -r '.mood // "focused"' "$STATE" 2>/dev/null)
|
|
63
66
|
|
|
@@ -229,10 +232,10 @@ DETECTED_COLS="$COLS"
|
|
|
229
232
|
DETECTED_ROWS="$ROWS"
|
|
230
233
|
|
|
231
234
|
# ─── Reaction bubble (with TTL check) ────────────────────────────────────────
|
|
235
|
+
# The achievement banner is resolved further down, once REACTION_TTL is known —
|
|
236
|
+
# it expires on the same clock as a reaction. Without that it latches into the
|
|
237
|
+
# shared status.json and pins every session's bubble to the last trophy.
|
|
232
238
|
BUBBLE=""
|
|
233
|
-
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ] && [ "$ACHIEVEMENT" != "" ]; then
|
|
234
|
-
BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
235
|
-
fi
|
|
236
239
|
REACTION_TTL=900
|
|
237
240
|
INNER_W=44
|
|
238
241
|
MARGIN=8
|
|
@@ -307,6 +310,31 @@ _sweep_expired_reactions() {
|
|
|
307
310
|
}
|
|
308
311
|
|
|
309
312
|
_sweep_expired_reactions
|
|
313
|
+
|
|
314
|
+
# Achievement banner: shown only while fresh. ACHIEVEMENT_AT is epoch ms, written
|
|
315
|
+
# alongside the name by writeStatusState. A legacy status.json without the field
|
|
316
|
+
# (pre-upgrade, or a snapshot fixture) is treated as fresh — the next write from
|
|
317
|
+
# the server backfills it.
|
|
318
|
+
#
|
|
319
|
+
# Validity and age are separate gates on purpose. A zeroed or malformed
|
|
320
|
+
# achievementAt means "nothing pending" and must never render, including under
|
|
321
|
+
# reactionTTL=0 — that opt-out disables *expiry*, not the field's meaning.
|
|
322
|
+
if [ -n "$ACHIEVEMENT" ] && [ "$ACHIEVEMENT" != "null" ]; then
|
|
323
|
+
ACH_FRESH=1
|
|
324
|
+
if [ "$ACHIEVEMENT_AT" != "absent" ]; then
|
|
325
|
+
case "$ACHIEVEMENT_AT" in
|
|
326
|
+
''|0|*[!0-9]*) ACH_FRESH=0 ;;
|
|
327
|
+
*)
|
|
328
|
+
if [ "$REACTION_TTL" -gt 0 ] 2>/dev/null; then
|
|
329
|
+
ACH_AGE=$(( ($(date +%s) * 1000 - ACHIEVEMENT_AT) / 1000 ))
|
|
330
|
+
[ "$ACH_AGE" -ge "$REACTION_TTL" ] && ACH_FRESH=0
|
|
331
|
+
fi
|
|
332
|
+
;;
|
|
333
|
+
esac
|
|
334
|
+
fi
|
|
335
|
+
[ "$ACH_FRESH" -eq 1 ] && BUBBLE=$'\xf0\x9f\x8f\x86'" $ACHIEVEMENT"
|
|
336
|
+
fi
|
|
337
|
+
|
|
310
338
|
REACTION=$(jq -r '.reaction // ""' "$REACTION_FILE" 2>/dev/null)
|
|
311
339
|
if [ -n "$REACTION" ] && [ "$REACTION" != "null" ] && [ "$REACTION" != "" ]; then
|
|
312
340
|
FRESH=0
|