@kpnpm/homie 0.1.2 → 0.2.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/README.md +14 -1
- package/package.json +14 -2
- package/pi-extension/index.js +204 -0
- package/pi-extension/package.json +8 -0
package/README.md
CHANGED
|
@@ -82,6 +82,14 @@ codex plugin add homie@homie
|
|
|
82
82
|
Then open `/hooks` in Codex, trust the two lifecycle hooks, and start a new
|
|
83
83
|
thread.
|
|
84
84
|
|
|
85
|
+
**Pi (pi.dev):**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pi install npm:@kpnpm/homie
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Also works for Oh My Pi (`omp`), which runs Pi extensions unchanged.
|
|
92
|
+
|
|
85
93
|
**Any other agent:** copy [`AGENTS.md`](AGENTS.md) into your project, or ask
|
|
86
94
|
your agent to install [`skills/homie/SKILL.md`](skills/homie/SKILL.md) as a
|
|
87
95
|
skill. More in [INSTALL.md](INSTALL.md).
|
|
@@ -100,7 +108,11 @@ down one. "stop homie" turns it off.
|
|
|
100
108
|
|
|
101
109
|
Levels persist for the whole session — turn it on once, it holds through
|
|
102
110
|
tool calls, long outputs, and topic changes. New sessions start at your
|
|
103
|
-
configured default (**yo** out of the box)
|
|
111
|
+
configured default (**yo** out of the box); in Pi the level is scoped to the
|
|
112
|
+
session and follows branch navigation, while OpenCode keeps the last level you
|
|
113
|
+
set across sessions. A plain-message nudge ("be blunter") shifts the voice for
|
|
114
|
+
that reply but does not move the persisted level — `/homie <level>` is the real
|
|
115
|
+
switch.
|
|
104
116
|
|
|
105
117
|
## Settings
|
|
106
118
|
|
|
@@ -127,6 +139,7 @@ loaded every turn:
|
|
|
127
139
|
|
|
128
140
|
- **Claude Code / Codex:** `SessionStart` injects the ruleset; `UserPromptSubmit` tracks `/homie` switches mid-session.
|
|
129
141
|
- **OpenCode:** a V2 plugin registers the `/homie` command and pushes the policy into the system context on every model call.
|
|
142
|
+
- **Pi (pi.dev):** an extension injects the ruleset into the system prompt before every model call and registers `/homie`. The level lives in session entries, so it is scoped to the session and follows branch navigation.
|
|
130
143
|
- **Others:** the `AGENTS.md` rules file.
|
|
131
144
|
|
|
132
145
|
Subagents don't get the voice — subagent prose isn't user-facing.
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kpnpm/homie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Personality layer for coding agents. Same code, different voice: yo / dawg / mafa / off.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode-plugin",
|
|
7
7
|
"opencode",
|
|
8
8
|
"claude-code-plugin",
|
|
9
9
|
"claude",
|
|
10
|
+
"pi",
|
|
11
|
+
"pi-package",
|
|
10
12
|
"skills",
|
|
11
13
|
"prompt-engineering"
|
|
12
14
|
],
|
|
@@ -35,10 +37,20 @@
|
|
|
35
37
|
".opencode/",
|
|
36
38
|
".claude-plugin/",
|
|
37
39
|
"commands/",
|
|
40
|
+
"pi-extension/",
|
|
41
|
+
"!pi-extension/test/",
|
|
38
42
|
"LICENSE"
|
|
39
43
|
],
|
|
44
|
+
"pi": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./pi-extension/index.js"
|
|
47
|
+
],
|
|
48
|
+
"skills": [
|
|
49
|
+
"./skills"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
40
52
|
"scripts": {
|
|
41
|
-
"test": "node --test \"tests/*.test.js\""
|
|
53
|
+
"test": "node --test \"tests/*.test.js\" && npm test --prefix pi-extension"
|
|
42
54
|
},
|
|
43
55
|
"publishConfig": {
|
|
44
56
|
"access": "public"
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// homie — Pi agent harness extension.
|
|
2
|
+
//
|
|
3
|
+
// Injects the homie ruleset into the system prompt before every model call
|
|
4
|
+
// (off = silence) and registers the /homie command. The level lives in session
|
|
5
|
+
// entries, so it is scoped to the session and follows branch navigation; the
|
|
6
|
+
// configured default (HOMIE_DEFAULT_LEVEL or ~/.config/homie/config.json)
|
|
7
|
+
// governs new sessions.
|
|
8
|
+
//
|
|
9
|
+
// The ruleset itself is read from ../hooks/, shared with every other homie
|
|
10
|
+
// host — no copy, no drift. This file is ESM; the hooks are CommonJS, hence
|
|
11
|
+
// the createRequire bridge below.
|
|
12
|
+
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const {
|
|
17
|
+
DEFAULT_LEVEL,
|
|
18
|
+
RUNTIME_LEVELS,
|
|
19
|
+
getDefaultLevel,
|
|
20
|
+
normalizeLevel,
|
|
21
|
+
isDeactivationCommand,
|
|
22
|
+
writeDefaultLevel,
|
|
23
|
+
} = require("../hooks/homie-config.js");
|
|
24
|
+
const { getHomieInstructions } = require("../hooks/homie-instructions.js");
|
|
25
|
+
|
|
26
|
+
const LEVEL_LIST = RUNTIME_LEVELS.join("|");
|
|
27
|
+
const LEVELS_MESSAGE = RUNTIME_LEVELS.join(", ");
|
|
28
|
+
const HOMIE_COMMAND_DESCRIPTION =
|
|
29
|
+
`Set voice level: ${LEVEL_LIST}. Commands: status, default <level>`;
|
|
30
|
+
|
|
31
|
+
// Parse the argument string of `/homie ...`.
|
|
32
|
+
//
|
|
33
|
+
// Bare `/homie` turns the voice on at yo when it is off, and reports the
|
|
34
|
+
// current level when it is already on — the SKILL.md contract, deliberately
|
|
35
|
+
// not the configured default.
|
|
36
|
+
export function parseHomieCommand(text, currentLevel = null) {
|
|
37
|
+
const normalized = String(text || "").trim().toLowerCase();
|
|
38
|
+
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
if (currentLevel && currentLevel !== "off") return { type: "report" };
|
|
41
|
+
return { type: "set-level", level: "yo" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const [primary, secondary] = normalized.split(/\s+/);
|
|
45
|
+
|
|
46
|
+
if (primary === "status") return { type: "status" };
|
|
47
|
+
|
|
48
|
+
if (primary === "default") {
|
|
49
|
+
const level = normalizeLevel(secondary);
|
|
50
|
+
return level
|
|
51
|
+
? { type: "set-default", level }
|
|
52
|
+
: { type: "invalid", reason: "invalid-default-level", level: secondary };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const level = normalizeLevel(primary);
|
|
56
|
+
return level
|
|
57
|
+
? { type: "set-level", level }
|
|
58
|
+
: { type: "invalid", reason: "invalid-level", level: primary };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Newest homie-mode entry on the active branch wins; otherwise the fallback.
|
|
62
|
+
export function resolveSessionLevel(entries, fallbackLevel = DEFAULT_LEVEL) {
|
|
63
|
+
const fallback = normalizeLevel(fallbackLevel) || DEFAULT_LEVEL;
|
|
64
|
+
if (!Array.isArray(entries)) return fallback;
|
|
65
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
66
|
+
const entry = entries[i];
|
|
67
|
+
if (entry?.type !== "custom" || entry?.customType !== "homie-mode") continue;
|
|
68
|
+
const level = normalizeLevel(entry?.data?.level);
|
|
69
|
+
if (level) return level;
|
|
70
|
+
}
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export { writeDefaultLevel };
|
|
75
|
+
export const readDefaultLevel = getDefaultLevel;
|
|
76
|
+
|
|
77
|
+
export default function homieExtension(pi) {
|
|
78
|
+
let currentLevel = DEFAULT_LEVEL;
|
|
79
|
+
let configuredDefault = getDefaultLevel();
|
|
80
|
+
let lastCtx = null;
|
|
81
|
+
|
|
82
|
+
function syncStatus(ctx) {
|
|
83
|
+
if (ctx) lastCtx = ctx;
|
|
84
|
+
const c = ctx || lastCtx;
|
|
85
|
+
if (!c?.ui?.setStatus) return;
|
|
86
|
+
// Plain text, no icons: "homie: dawg". Cleared when off.
|
|
87
|
+
c.ui.setStatus("homie", currentLevel === "off" ? undefined : `homie: ${currentLevel}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const setLevel = (level, ctx) => {
|
|
91
|
+
const normalized = normalizeLevel(level);
|
|
92
|
+
if (!normalized) return;
|
|
93
|
+
currentLevel = normalized;
|
|
94
|
+
pi.appendEntry("homie-mode", { level: normalized });
|
|
95
|
+
syncStatus(ctx);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const notify = (ctx, message, type = "info") => ctx?.ui?.notify?.(message, type);
|
|
99
|
+
|
|
100
|
+
pi.registerCommand("homie", {
|
|
101
|
+
description: HOMIE_COMMAND_DESCRIPTION,
|
|
102
|
+
handler: async (args, ctx) => {
|
|
103
|
+
const parsed = parseHomieCommand(args, currentLevel);
|
|
104
|
+
|
|
105
|
+
if (parsed.type === "status") {
|
|
106
|
+
notify(ctx, `Homie: current ${currentLevel} • default ${configuredDefault}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (parsed.type === "report") {
|
|
111
|
+
// Bare /homie while active: report the level, change nothing.
|
|
112
|
+
notify(ctx, `Homie mode: ${currentLevel}.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (parsed.type === "set-default") {
|
|
117
|
+
try {
|
|
118
|
+
const written = writeDefaultLevel(parsed.level);
|
|
119
|
+
if (!written) return;
|
|
120
|
+
configuredDefault = getDefaultLevel();
|
|
121
|
+
notify(
|
|
122
|
+
ctx,
|
|
123
|
+
configuredDefault === written
|
|
124
|
+
? `Homie default set: ${written}. New sessions start at ${written}.`
|
|
125
|
+
: `Saved default ${written}, but HOMIE_DEFAULT_LEVEL keeps default at ${configuredDefault}.`,
|
|
126
|
+
);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
notify(ctx, `Failed to save default: ${e.message}`, "error");
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (parsed.type === "set-level") {
|
|
134
|
+
setLevel(parsed.level, ctx);
|
|
135
|
+
notify(ctx, currentLevel === "off" ? "Homie off." : `Homie mode: ${currentLevel}.`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// invalid
|
|
140
|
+
if (parsed.reason === "invalid-default-level") {
|
|
141
|
+
notify(ctx, `Usage: /homie default <level>. Levels: ${LEVELS_MESSAGE}.`, "warning");
|
|
142
|
+
} else {
|
|
143
|
+
notify(ctx, `Unknown level "${parsed.level}". Levels: ${LEVELS_MESSAGE}.`, "warning");
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.on("input", async (event, ctx) => {
|
|
149
|
+
if (event?.source === "extension") return;
|
|
150
|
+
const text = String(event?.text || "");
|
|
151
|
+
if (currentLevel !== "off" && isDeactivationCommand(text)) {
|
|
152
|
+
setLevel("off", ctx);
|
|
153
|
+
notify(ctx, "Homie off.");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const restoreSessionLevel = (ctx) => {
|
|
158
|
+
const entries =
|
|
159
|
+
ctx?.sessionManager?.getBranch?.() ||
|
|
160
|
+
ctx?.sessionManager?.getEntries?.() ||
|
|
161
|
+
[];
|
|
162
|
+
currentLevel = resolveSessionLevel(entries, configuredDefault);
|
|
163
|
+
syncStatus(ctx);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
167
|
+
configuredDefault = getDefaultLevel();
|
|
168
|
+
restoreSessionLevel(ctx);
|
|
169
|
+
notify(ctx, `Homie loaded: ${currentLevel}`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Branch navigation is a new view of history: re-derive the level from the
|
|
173
|
+
// active branch rather than assuming the previous one still applies.
|
|
174
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
175
|
+
restoreSessionLevel(ctx);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
pi.on("before_agent_start", async (event) => {
|
|
179
|
+
if (!currentLevel || currentLevel === "off") {
|
|
180
|
+
// Clear a stale section so a previous turn's voice does not linger.
|
|
181
|
+
if (event?.systemPromptOptions?.sections) {
|
|
182
|
+
delete event.systemPromptOptions.sections.homie;
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const instructions = getHomieInstructions(currentLevel);
|
|
188
|
+
|
|
189
|
+
// Prefer structured sections so Pi can keep its cached prefix when other
|
|
190
|
+
// extensions change their own section. Fall back to the array form (omp)
|
|
191
|
+
// and then to replacing the system prompt, guarding a null/missing prompt
|
|
192
|
+
// so the literal string "undefined" is never prepended.
|
|
193
|
+
const sections = event?.systemPromptOptions?.sections;
|
|
194
|
+
if (sections && typeof sections === "object") {
|
|
195
|
+
sections.homie = instructions;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(event?.systemPrompt)) {
|
|
199
|
+
return { systemPrompt: [...event.systemPrompt, instructions] };
|
|
200
|
+
}
|
|
201
|
+
const base = event?.systemPrompt ? `${event.systemPrompt}\n\n` : "";
|
|
202
|
+
return { systemPrompt: `${base}${instructions}` };
|
|
203
|
+
});
|
|
204
|
+
}
|