@akonwi/opencode-kit 0.1.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 +21 -0
- package/README.md +67 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +175 -0
- package/dist/cli.js.map +1 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +335 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akonwi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @akonwi/opencode-kit
|
|
2
|
+
|
|
3
|
+
OpenCode plugin package for local workflow notifications and runtime toggles.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Idle notification with terminal bell and optional speech.
|
|
8
|
+
- Error notification with macOS Funk sound and optional speech.
|
|
9
|
+
- Runtime config reads from `~/.config/opencode/kit.json`.
|
|
10
|
+
- Structured JSON-line logs at `~/.config/opencode/logs/opencode-kit.log`.
|
|
11
|
+
- Local control CLI (no AI turn): `oc-kit`.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @akonwi/opencode-kit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Register the plugin in your OpenCode plugin config by loading `@akonwi/opencode-kit` and the `OpencodeKit` export.
|
|
20
|
+
|
|
21
|
+
## Config
|
|
22
|
+
|
|
23
|
+
File: `~/.config/opencode/kit.json`
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"bells": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"errorSound": "Funk"
|
|
30
|
+
},
|
|
31
|
+
"speech": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"maxChars": 220,
|
|
34
|
+
"voice": null
|
|
35
|
+
},
|
|
36
|
+
"debug": {
|
|
37
|
+
"logLevel": "info"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If the file is missing or invalid, safe defaults are used.
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
oc-kit bells on
|
|
48
|
+
oc-kit bells off
|
|
49
|
+
oc-kit bells toggle
|
|
50
|
+
oc-kit bells status
|
|
51
|
+
|
|
52
|
+
oc-kit speech on
|
|
53
|
+
oc-kit speech off
|
|
54
|
+
oc-kit speech toggle
|
|
55
|
+
oc-kit speech status
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The plugin re-reads `kit.json` at runtime so changes apply without restart.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun install
|
|
64
|
+
bun run format
|
|
65
|
+
bun run lint
|
|
66
|
+
bun run build
|
|
67
|
+
```
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var CONFIG_PATH = path.join(homedir(), ".config", "opencode", "kit.json");
|
|
8
|
+
var VALID_LOG_LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
9
|
+
var DEFAULT_CONFIG = {
|
|
10
|
+
bells: {
|
|
11
|
+
enabled: true,
|
|
12
|
+
errorSound: "Funk"
|
|
13
|
+
},
|
|
14
|
+
speech: {
|
|
15
|
+
enabled: true,
|
|
16
|
+
maxChars: 220,
|
|
17
|
+
voice: null
|
|
18
|
+
},
|
|
19
|
+
debug: {
|
|
20
|
+
logLevel: "info"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null;
|
|
25
|
+
}
|
|
26
|
+
function asBoolean(value, fallback) {
|
|
27
|
+
return typeof value === "boolean" ? value : fallback;
|
|
28
|
+
}
|
|
29
|
+
function asErrorSound(value, fallback) {
|
|
30
|
+
return value === "Funk" ? "Funk" : fallback;
|
|
31
|
+
}
|
|
32
|
+
function asOptionalString(value, fallback) {
|
|
33
|
+
if (value === null) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return typeof value === "string" && value.trim() !== "" ? value : fallback;
|
|
37
|
+
}
|
|
38
|
+
function asIntInRange(value, fallback, min, max) {
|
|
39
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
if (value < min || value > max) {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function asLogLevel(value, fallback) {
|
|
48
|
+
if (typeof value !== "string") {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
return VALID_LOG_LEVELS.has(value) ? value : fallback;
|
|
52
|
+
}
|
|
53
|
+
function sanitizeConfig(input) {
|
|
54
|
+
const raw = isRecord(input) ? input : {};
|
|
55
|
+
const rawBells = isRecord(raw.bells) ? raw.bells : {};
|
|
56
|
+
const rawSpeech = isRecord(raw.speech) ? raw.speech : {};
|
|
57
|
+
const rawDebug = isRecord(raw.debug) ? raw.debug : {};
|
|
58
|
+
return {
|
|
59
|
+
bells: {
|
|
60
|
+
enabled: asBoolean(rawBells.enabled, DEFAULT_CONFIG.bells.enabled),
|
|
61
|
+
errorSound: asErrorSound(rawBells.errorSound, DEFAULT_CONFIG.bells.errorSound)
|
|
62
|
+
},
|
|
63
|
+
speech: {
|
|
64
|
+
enabled: asBoolean(rawSpeech.enabled, DEFAULT_CONFIG.speech.enabled),
|
|
65
|
+
maxChars: asIntInRange(rawSpeech.maxChars, DEFAULT_CONFIG.speech.maxChars, 20, 2e3),
|
|
66
|
+
voice: asOptionalString(rawSpeech.voice, DEFAULT_CONFIG.speech.voice)
|
|
67
|
+
},
|
|
68
|
+
debug: {
|
|
69
|
+
logLevel: asLogLevel(rawDebug.logLevel, DEFAULT_CONFIG.debug.logLevel)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function readConfig() {
|
|
74
|
+
try {
|
|
75
|
+
const content = await readFile(CONFIG_PATH, "utf8");
|
|
76
|
+
const parsed = JSON.parse(content);
|
|
77
|
+
return sanitizeConfig(parsed);
|
|
78
|
+
} catch {
|
|
79
|
+
return sanitizeConfig(DEFAULT_CONFIG);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function ensureConfigDirectory() {
|
|
83
|
+
await mkdir(path.dirname(CONFIG_PATH), { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
async function writeConfig(config) {
|
|
86
|
+
const safeConfig = sanitizeConfig(config);
|
|
87
|
+
const serialized = `${JSON.stringify(safeConfig, null, 2)}
|
|
88
|
+
`;
|
|
89
|
+
const tempPath = `${CONFIG_PATH}.tmp`;
|
|
90
|
+
await ensureConfigDirectory();
|
|
91
|
+
await writeFile(tempPath, serialized, "utf8");
|
|
92
|
+
await rename(tempPath, CONFIG_PATH);
|
|
93
|
+
}
|
|
94
|
+
async function updateConfig(mutator) {
|
|
95
|
+
const current = await readConfig();
|
|
96
|
+
const next = sanitizeConfig(mutator(current));
|
|
97
|
+
await writeConfig(next);
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/cli.ts
|
|
102
|
+
function printUsage() {
|
|
103
|
+
const lines = [
|
|
104
|
+
"Usage:",
|
|
105
|
+
" oc-kit bells on|off|toggle|status",
|
|
106
|
+
" oc-kit speech on|off|toggle|status"
|
|
107
|
+
];
|
|
108
|
+
process.stdout.write(`${lines.join("\n")}
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
function asTopic(input) {
|
|
112
|
+
if (input === "bells" || input === "speech") {
|
|
113
|
+
return input;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function asAction(input) {
|
|
118
|
+
if (input === "on" || input === "off" || input === "toggle" || input === "status") {
|
|
119
|
+
return input;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
async function getStatusLine() {
|
|
124
|
+
const config = await readConfig();
|
|
125
|
+
return `bells=${config.bells.enabled ? "on" : "off"} speech=${config.speech.enabled ? "on" : "off"}`;
|
|
126
|
+
}
|
|
127
|
+
async function run() {
|
|
128
|
+
const args = process.argv.slice(2);
|
|
129
|
+
const topic = asTopic(args[0]);
|
|
130
|
+
const action = asAction(args[1]);
|
|
131
|
+
if (!topic || !action) {
|
|
132
|
+
printUsage();
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (action === "status") {
|
|
137
|
+
const line = await getStatusLine();
|
|
138
|
+
process.stdout.write(`${line}
|
|
139
|
+
config=${CONFIG_PATH}
|
|
140
|
+
`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const updated = await updateConfig((current) => {
|
|
144
|
+
const enabled = topic === "bells" ? current.bells.enabled : current.speech.enabled;
|
|
145
|
+
const nextEnabled = action === "toggle" ? !enabled : action === "on";
|
|
146
|
+
if (topic === "bells") {
|
|
147
|
+
return {
|
|
148
|
+
...current,
|
|
149
|
+
bells: {
|
|
150
|
+
...current.bells,
|
|
151
|
+
enabled: nextEnabled
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
...current,
|
|
157
|
+
speech: {
|
|
158
|
+
...current.speech,
|
|
159
|
+
enabled: nextEnabled
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`updated ${topic}.enabled=${topic === "bells" ? updated.bells.enabled : updated.speech.enabled}
|
|
165
|
+
`
|
|
166
|
+
);
|
|
167
|
+
process.stdout.write(
|
|
168
|
+
`status bells=${updated.bells.enabled ? "on" : "off"} speech=${updated.speech.enabled ? "on" : "off"}
|
|
169
|
+
`
|
|
170
|
+
);
|
|
171
|
+
process.stdout.write(`config=${CONFIG_PATH}
|
|
172
|
+
`);
|
|
173
|
+
}
|
|
174
|
+
void run();
|
|
175
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/cli.ts"],"sourcesContent":["import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface KitConfig {\n bells: {\n enabled: boolean;\n errorSound: \"Funk\";\n };\n speech: {\n enabled: boolean;\n maxChars: number;\n voice: string | null;\n };\n debug: {\n logLevel: LogLevel;\n };\n}\n\nexport const CONFIG_PATH = path.join(homedir(), \".config\", \"opencode\", \"kit.json\");\n\nconst VALID_LOG_LEVELS: ReadonlySet<string> = new Set([\"debug\", \"info\", \"warn\", \"error\"]);\n\nconst DEFAULT_CONFIG: KitConfig = {\n bells: {\n enabled: true,\n errorSound: \"Funk\",\n },\n speech: {\n enabled: true,\n maxChars: 220,\n voice: null,\n },\n debug: {\n logLevel: \"info\",\n },\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction asErrorSound(value: unknown, fallback: \"Funk\"): \"Funk\" {\n return value === \"Funk\" ? \"Funk\" : fallback;\n}\n\nfunction asOptionalString(value: unknown, fallback: string | null): string | null {\n if (value === null) {\n return null;\n }\n\n return typeof value === \"string\" && value.trim() !== \"\" ? value : fallback;\n}\n\nfunction asIntInRange(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== \"number\" || !Number.isInteger(value)) {\n return fallback;\n }\n\n if (value < min || value > max) {\n return fallback;\n }\n\n return value;\n}\n\nfunction asLogLevel(value: unknown, fallback: LogLevel): LogLevel {\n if (typeof value !== \"string\") {\n return fallback;\n }\n\n return VALID_LOG_LEVELS.has(value) ? (value as LogLevel) : fallback;\n}\n\nexport function sanitizeConfig(input: unknown): KitConfig {\n const raw = isRecord(input) ? input : {};\n const rawBells = isRecord(raw.bells) ? raw.bells : {};\n const rawSpeech = isRecord(raw.speech) ? raw.speech : {};\n const rawDebug = isRecord(raw.debug) ? raw.debug : {};\n\n return {\n bells: {\n enabled: asBoolean(rawBells.enabled, DEFAULT_CONFIG.bells.enabled),\n errorSound: asErrorSound(rawBells.errorSound, DEFAULT_CONFIG.bells.errorSound),\n },\n speech: {\n enabled: asBoolean(rawSpeech.enabled, DEFAULT_CONFIG.speech.enabled),\n maxChars: asIntInRange(rawSpeech.maxChars, DEFAULT_CONFIG.speech.maxChars, 20, 2000),\n voice: asOptionalString(rawSpeech.voice, DEFAULT_CONFIG.speech.voice),\n },\n debug: {\n logLevel: asLogLevel(rawDebug.logLevel, DEFAULT_CONFIG.debug.logLevel),\n },\n };\n}\n\nexport async function readConfig(): Promise<KitConfig> {\n try {\n const content = await readFile(CONFIG_PATH, \"utf8\");\n const parsed = JSON.parse(content) as unknown;\n return sanitizeConfig(parsed);\n } catch {\n return sanitizeConfig(DEFAULT_CONFIG);\n }\n}\n\nasync function ensureConfigDirectory(): Promise<void> {\n await mkdir(path.dirname(CONFIG_PATH), { recursive: true });\n}\n\nexport async function writeConfig(config: KitConfig): Promise<void> {\n const safeConfig = sanitizeConfig(config);\n const serialized = `${JSON.stringify(safeConfig, null, 2)}\\n`;\n const tempPath = `${CONFIG_PATH}.tmp`;\n\n await ensureConfigDirectory();\n await writeFile(tempPath, serialized, \"utf8\");\n await rename(tempPath, CONFIG_PATH);\n}\n\nexport async function updateConfig(mutator: (current: KitConfig) => KitConfig): Promise<KitConfig> {\n const current = await readConfig();\n const next = sanitizeConfig(mutator(current));\n await writeConfig(next);\n return next;\n}\n\nexport function defaultConfig(): KitConfig {\n return sanitizeConfig(DEFAULT_CONFIG);\n}\n","#!/usr/bin/env node\n\nimport { CONFIG_PATH, readConfig, updateConfig } from \"./config\";\n\ntype Topic = \"bells\" | \"speech\";\ntype Action = \"on\" | \"off\" | \"toggle\" | \"status\";\n\nfunction printUsage(): void {\n const lines = [\n \"Usage:\",\n \" oc-kit bells on|off|toggle|status\",\n \" oc-kit speech on|off|toggle|status\",\n ];\n process.stdout.write(`${lines.join(\"\\n\")}\\n`);\n}\n\nfunction asTopic(input: string | undefined): Topic | null {\n if (input === \"bells\" || input === \"speech\") {\n return input;\n }\n\n return null;\n}\n\nfunction asAction(input: string | undefined): Action | null {\n if (input === \"on\" || input === \"off\" || input === \"toggle\" || input === \"status\") {\n return input;\n }\n\n return null;\n}\n\nasync function getStatusLine(): Promise<string> {\n const config = await readConfig();\n return `bells=${config.bells.enabled ? \"on\" : \"off\"} speech=${config.speech.enabled ? \"on\" : \"off\"}`;\n}\n\nasync function run(): Promise<void> {\n const args = process.argv.slice(2);\n const topic = asTopic(args[0]);\n const action = asAction(args[1]);\n\n if (!topic || !action) {\n printUsage();\n process.exitCode = 1;\n return;\n }\n\n if (action === \"status\") {\n const line = await getStatusLine();\n process.stdout.write(`${line}\\nconfig=${CONFIG_PATH}\\n`);\n return;\n }\n\n const updated = await updateConfig((current) => {\n const enabled = topic === \"bells\" ? current.bells.enabled : current.speech.enabled;\n const nextEnabled = action === \"toggle\" ? !enabled : action === \"on\";\n\n if (topic === \"bells\") {\n return {\n ...current,\n bells: {\n ...current.bells,\n enabled: nextEnabled,\n },\n };\n }\n\n return {\n ...current,\n speech: {\n ...current.speech,\n enabled: nextEnabled,\n },\n };\n });\n\n process.stdout.write(\n `updated ${topic}.enabled=${topic === \"bells\" ? updated.bells.enabled : updated.speech.enabled}\\n`,\n );\n process.stdout.write(\n `status bells=${updated.bells.enabled ? \"on\" : \"off\"} speech=${updated.speech.enabled ? \"on\" : \"off\"}\\n`,\n );\n process.stdout.write(`config=${CONFIG_PATH}\\n`);\n}\n\nvoid run();\n"],"mappings":"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","names":[]}
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var CONFIG_PATH = path.join(homedir(), ".config", "opencode", "kit.json");
|
|
6
|
+
var VALID_LOG_LEVELS = /* @__PURE__ */ new Set(["debug", "info", "warn", "error"]);
|
|
7
|
+
var DEFAULT_CONFIG = {
|
|
8
|
+
bells: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
errorSound: "Funk"
|
|
11
|
+
},
|
|
12
|
+
speech: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
maxChars: 220,
|
|
15
|
+
voice: null
|
|
16
|
+
},
|
|
17
|
+
debug: {
|
|
18
|
+
logLevel: "info"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null;
|
|
23
|
+
}
|
|
24
|
+
function asBoolean(value, fallback) {
|
|
25
|
+
return typeof value === "boolean" ? value : fallback;
|
|
26
|
+
}
|
|
27
|
+
function asErrorSound(value, fallback) {
|
|
28
|
+
return value === "Funk" ? "Funk" : fallback;
|
|
29
|
+
}
|
|
30
|
+
function asOptionalString(value, fallback) {
|
|
31
|
+
if (value === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return typeof value === "string" && value.trim() !== "" ? value : fallback;
|
|
35
|
+
}
|
|
36
|
+
function asIntInRange(value, fallback, min, max) {
|
|
37
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
if (value < min || value > max) {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function asLogLevel(value, fallback) {
|
|
46
|
+
if (typeof value !== "string") {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
return VALID_LOG_LEVELS.has(value) ? value : fallback;
|
|
50
|
+
}
|
|
51
|
+
function sanitizeConfig(input) {
|
|
52
|
+
const raw = isRecord(input) ? input : {};
|
|
53
|
+
const rawBells = isRecord(raw.bells) ? raw.bells : {};
|
|
54
|
+
const rawSpeech = isRecord(raw.speech) ? raw.speech : {};
|
|
55
|
+
const rawDebug = isRecord(raw.debug) ? raw.debug : {};
|
|
56
|
+
return {
|
|
57
|
+
bells: {
|
|
58
|
+
enabled: asBoolean(rawBells.enabled, DEFAULT_CONFIG.bells.enabled),
|
|
59
|
+
errorSound: asErrorSound(rawBells.errorSound, DEFAULT_CONFIG.bells.errorSound)
|
|
60
|
+
},
|
|
61
|
+
speech: {
|
|
62
|
+
enabled: asBoolean(rawSpeech.enabled, DEFAULT_CONFIG.speech.enabled),
|
|
63
|
+
maxChars: asIntInRange(rawSpeech.maxChars, DEFAULT_CONFIG.speech.maxChars, 20, 2e3),
|
|
64
|
+
voice: asOptionalString(rawSpeech.voice, DEFAULT_CONFIG.speech.voice)
|
|
65
|
+
},
|
|
66
|
+
debug: {
|
|
67
|
+
logLevel: asLogLevel(rawDebug.logLevel, DEFAULT_CONFIG.debug.logLevel)
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function readConfig() {
|
|
72
|
+
try {
|
|
73
|
+
const content = await readFile(CONFIG_PATH, "utf8");
|
|
74
|
+
const parsed = JSON.parse(content);
|
|
75
|
+
return sanitizeConfig(parsed);
|
|
76
|
+
} catch {
|
|
77
|
+
return sanitizeConfig(DEFAULT_CONFIG);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/features/logging.ts
|
|
82
|
+
import { appendFile, mkdir as mkdir2 } from "fs/promises";
|
|
83
|
+
import { homedir as homedir2 } from "os";
|
|
84
|
+
import path2 from "path";
|
|
85
|
+
var LOG_DIR = path2.join(homedir2(), ".config", "opencode", "logs");
|
|
86
|
+
var LOG_PATH = path2.join(LOG_DIR, "opencode-kit.log");
|
|
87
|
+
var LOG_WEIGHT = {
|
|
88
|
+
debug: 10,
|
|
89
|
+
info: 20,
|
|
90
|
+
warn: 30,
|
|
91
|
+
error: 40
|
|
92
|
+
};
|
|
93
|
+
function shouldLog(minLevel, level) {
|
|
94
|
+
return LOG_WEIGHT[level] >= LOG_WEIGHT[minLevel];
|
|
95
|
+
}
|
|
96
|
+
async function appendLogLine(line) {
|
|
97
|
+
try {
|
|
98
|
+
await mkdir2(LOG_DIR, { recursive: true });
|
|
99
|
+
await appendFile(LOG_PATH, `${JSON.stringify(line)}
|
|
100
|
+
`, "utf8");
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function createLogger(initialLevel = "info") {
|
|
105
|
+
let minLevel = initialLevel;
|
|
106
|
+
const write = (level, event, message, context) => {
|
|
107
|
+
if (!shouldLog(minLevel, level)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
void appendLogLine({
|
|
111
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
112
|
+
level,
|
|
113
|
+
event,
|
|
114
|
+
message,
|
|
115
|
+
context
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
setLevel: (level) => {
|
|
120
|
+
minLevel = level;
|
|
121
|
+
},
|
|
122
|
+
debug: (event, message, context) => {
|
|
123
|
+
write("debug", event, message, context);
|
|
124
|
+
},
|
|
125
|
+
info: (event, message, context) => {
|
|
126
|
+
write("info", event, message, context);
|
|
127
|
+
},
|
|
128
|
+
warn: (event, message, context) => {
|
|
129
|
+
write("warn", event, message, context);
|
|
130
|
+
},
|
|
131
|
+
error: (event, message, context) => {
|
|
132
|
+
write("error", event, message, context);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/features/sounds.ts
|
|
138
|
+
import { spawn } from "child_process";
|
|
139
|
+
var FUNK_SOUND_PATH = "/System/Library/Sounds/Funk.aiff";
|
|
140
|
+
function runCommand(command, args, logger) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const child = spawn(command, args, {
|
|
143
|
+
stdio: "ignore"
|
|
144
|
+
});
|
|
145
|
+
child.on("error", (error) => {
|
|
146
|
+
logger.warn("command.error", "Command failed to launch", {
|
|
147
|
+
command,
|
|
148
|
+
args,
|
|
149
|
+
error: error.message
|
|
150
|
+
});
|
|
151
|
+
resolve();
|
|
152
|
+
});
|
|
153
|
+
child.on("exit", (code) => {
|
|
154
|
+
if (code !== 0) {
|
|
155
|
+
logger.warn("command.nonzero", "Command exited non-zero", {
|
|
156
|
+
command,
|
|
157
|
+
args,
|
|
158
|
+
code
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
resolve();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function truncateSummary(text, maxChars) {
|
|
166
|
+
const trimmed = text.trim();
|
|
167
|
+
if (trimmed.length <= maxChars) {
|
|
168
|
+
return trimmed;
|
|
169
|
+
}
|
|
170
|
+
return `${trimmed.slice(0, maxChars - 3)}...`;
|
|
171
|
+
}
|
|
172
|
+
function cleanTextForSpeech(text) {
|
|
173
|
+
return text.replace(/```[\s\S]*?```/g, " code block omitted ").replace(/`([^`]+)`/g, "$1").replace(/\[(.*?)\]\((.*?)\)/g, "$1").replace(/[*_~#>]/g, "").replace(/\s+/g, " ").trim();
|
|
174
|
+
}
|
|
175
|
+
function shortSpeech(text, maxChars) {
|
|
176
|
+
const cleaned = cleanTextForSpeech(text);
|
|
177
|
+
if (!cleaned) {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
if (cleaned.length <= maxChars) {
|
|
181
|
+
return cleaned;
|
|
182
|
+
}
|
|
183
|
+
const sentence = cleaned.match(/(.+?[.!?])(\s|$)/)?.[1]?.trim();
|
|
184
|
+
if (sentence && sentence.length <= maxChars) {
|
|
185
|
+
return sentence;
|
|
186
|
+
}
|
|
187
|
+
return truncateSummary(cleaned, maxChars);
|
|
188
|
+
}
|
|
189
|
+
function writeTerminalBell(logger) {
|
|
190
|
+
try {
|
|
191
|
+
process.stdout.write("\x07");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.warn("bell.error", "Failed to write terminal bell", {
|
|
194
|
+
error: error instanceof Error ? error.message : String(error)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function speak(text, config, logger) {
|
|
199
|
+
if (!config.speech.enabled) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (process.platform !== "darwin") {
|
|
203
|
+
logger.info("speech.unsupported", "Speech skipped: platform unsupported", {
|
|
204
|
+
platform: process.platform
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const clipped = shortSpeech(text, config.speech.maxChars);
|
|
209
|
+
if (!clipped) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const args = [];
|
|
213
|
+
if (config.speech.voice) {
|
|
214
|
+
args.push("-v", config.speech.voice);
|
|
215
|
+
}
|
|
216
|
+
args.push(clipped);
|
|
217
|
+
await runCommand("say", args, logger);
|
|
218
|
+
}
|
|
219
|
+
async function playErrorSound(config, logger) {
|
|
220
|
+
if (!config.bells.enabled) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (process.platform !== "darwin") {
|
|
224
|
+
writeTerminalBell(logger);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (config.bells.errorSound === "Funk") {
|
|
228
|
+
await runCommand("afplay", [FUNK_SOUND_PATH], logger);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function notifyIdle(lastAssistantText, config, logger) {
|
|
232
|
+
if (config.bells.enabled) {
|
|
233
|
+
writeTerminalBell(logger);
|
|
234
|
+
}
|
|
235
|
+
if (config.speech.enabled && lastAssistantText.trim()) {
|
|
236
|
+
await speak(lastAssistantText, config, logger);
|
|
237
|
+
}
|
|
238
|
+
logger.debug("idle.notify", "Idle notification processed", {
|
|
239
|
+
bell: config.bells.enabled,
|
|
240
|
+
speech: config.speech.enabled
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function notifyError(message, config, logger) {
|
|
244
|
+
await playErrorSound(config, logger);
|
|
245
|
+
if (config.speech.enabled) {
|
|
246
|
+
const safeMessage = message?.trim() || "Unknown error";
|
|
247
|
+
await speak(`OpenCode error: ${safeMessage}`, config, logger);
|
|
248
|
+
}
|
|
249
|
+
logger.warn("error.notify", "Error notification processed", {
|
|
250
|
+
bell: config.bells.enabled,
|
|
251
|
+
speech: config.speech.enabled
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/plugin.ts
|
|
256
|
+
function getErrorMessage(event) {
|
|
257
|
+
if (event.type !== "session.error") {
|
|
258
|
+
return "Unknown error";
|
|
259
|
+
}
|
|
260
|
+
const rawError = event.properties.error;
|
|
261
|
+
if (!rawError) {
|
|
262
|
+
return "Agent encountered an error.";
|
|
263
|
+
}
|
|
264
|
+
const errorData = rawError.data;
|
|
265
|
+
if (errorData && typeof errorData === "object") {
|
|
266
|
+
if ("message" in errorData && typeof errorData.message === "string" && errorData.message.trim()) {
|
|
267
|
+
return `Agent encountered an error: ${errorData.message.trim()}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (typeof rawError.name === "string" && rawError.name.trim()) {
|
|
271
|
+
return `Agent encountered an error: ${rawError.name.trim()}`;
|
|
272
|
+
}
|
|
273
|
+
return "Agent encountered an error.";
|
|
274
|
+
}
|
|
275
|
+
var OpencodeKit = async (input) => {
|
|
276
|
+
const logger = createLogger();
|
|
277
|
+
const latestAssistantMessageBySession = /* @__PURE__ */ new Map();
|
|
278
|
+
const latestAssistantTextByMessage = /* @__PURE__ */ new Map();
|
|
279
|
+
const lastSpokenMessageBySession = /* @__PURE__ */ new Map();
|
|
280
|
+
return {
|
|
281
|
+
event: async ({ event }) => {
|
|
282
|
+
if (event.type === "message.updated") {
|
|
283
|
+
const info = event.properties.info;
|
|
284
|
+
if (info.role === "assistant") {
|
|
285
|
+
latestAssistantMessageBySession.set(info.sessionID, info.id);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (event.type === "message.part.updated") {
|
|
290
|
+
const part = event.properties.part;
|
|
291
|
+
if (part.type === "text" && typeof part.messageID === "string" && typeof part.text === "string") {
|
|
292
|
+
latestAssistantTextByMessage.set(part.messageID, part.text);
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (event.type !== "session.idle" && event.type !== "session.error") {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const config = await readConfig();
|
|
300
|
+
logger.setLevel(config.debug.logLevel);
|
|
301
|
+
if (event.type === "session.idle") {
|
|
302
|
+
const sessionID = event.properties.sessionID;
|
|
303
|
+
const latestMessageID = latestAssistantMessageBySession.get(sessionID);
|
|
304
|
+
const lastText = latestMessageID ? latestAssistantTextByMessage.get(latestMessageID) ?? "" : "";
|
|
305
|
+
const previouslySpoken = lastSpokenMessageBySession.get(sessionID);
|
|
306
|
+
if (latestMessageID && previouslySpoken === latestMessageID) {
|
|
307
|
+
logger.debug("idle.skip_duplicate", "Skipping duplicate idle speech", {
|
|
308
|
+
sessionID,
|
|
309
|
+
messageID: latestMessageID
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (!latestMessageID || !lastText) {
|
|
314
|
+
logger.warn("idle.no_summary", "No cached assistant summary for idle event", {
|
|
315
|
+
sessionID,
|
|
316
|
+
hasMessageID: Boolean(latestMessageID),
|
|
317
|
+
hasText: Boolean(lastText)
|
|
318
|
+
});
|
|
319
|
+
await notifyIdle("", config, logger);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
lastSpokenMessageBySession.set(sessionID, latestMessageID);
|
|
323
|
+
await notifyIdle(lastText, config, logger);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
await notifyError(getErrorMessage(event), config, logger);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
};
|
|
330
|
+
var plugin_default = OpencodeKit;
|
|
331
|
+
export {
|
|
332
|
+
OpencodeKit,
|
|
333
|
+
plugin_default as default
|
|
334
|
+
};
|
|
335
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/features/logging.ts","../src/features/sounds.ts","../src/plugin.ts"],"sourcesContent":["import { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nexport interface KitConfig {\n bells: {\n enabled: boolean;\n errorSound: \"Funk\";\n };\n speech: {\n enabled: boolean;\n maxChars: number;\n voice: string | null;\n };\n debug: {\n logLevel: LogLevel;\n };\n}\n\nexport const CONFIG_PATH = path.join(homedir(), \".config\", \"opencode\", \"kit.json\");\n\nconst VALID_LOG_LEVELS: ReadonlySet<string> = new Set([\"debug\", \"info\", \"warn\", \"error\"]);\n\nconst DEFAULT_CONFIG: KitConfig = {\n bells: {\n enabled: true,\n errorSound: \"Funk\",\n },\n speech: {\n enabled: true,\n maxChars: 220,\n voice: null,\n },\n debug: {\n logLevel: \"info\",\n },\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction asErrorSound(value: unknown, fallback: \"Funk\"): \"Funk\" {\n return value === \"Funk\" ? \"Funk\" : fallback;\n}\n\nfunction asOptionalString(value: unknown, fallback: string | null): string | null {\n if (value === null) {\n return null;\n }\n\n return typeof value === \"string\" && value.trim() !== \"\" ? value : fallback;\n}\n\nfunction asIntInRange(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== \"number\" || !Number.isInteger(value)) {\n return fallback;\n }\n\n if (value < min || value > max) {\n return fallback;\n }\n\n return value;\n}\n\nfunction asLogLevel(value: unknown, fallback: LogLevel): LogLevel {\n if (typeof value !== \"string\") {\n return fallback;\n }\n\n return VALID_LOG_LEVELS.has(value) ? (value as LogLevel) : fallback;\n}\n\nexport function sanitizeConfig(input: unknown): KitConfig {\n const raw = isRecord(input) ? input : {};\n const rawBells = isRecord(raw.bells) ? raw.bells : {};\n const rawSpeech = isRecord(raw.speech) ? raw.speech : {};\n const rawDebug = isRecord(raw.debug) ? raw.debug : {};\n\n return {\n bells: {\n enabled: asBoolean(rawBells.enabled, DEFAULT_CONFIG.bells.enabled),\n errorSound: asErrorSound(rawBells.errorSound, DEFAULT_CONFIG.bells.errorSound),\n },\n speech: {\n enabled: asBoolean(rawSpeech.enabled, DEFAULT_CONFIG.speech.enabled),\n maxChars: asIntInRange(rawSpeech.maxChars, DEFAULT_CONFIG.speech.maxChars, 20, 2000),\n voice: asOptionalString(rawSpeech.voice, DEFAULT_CONFIG.speech.voice),\n },\n debug: {\n logLevel: asLogLevel(rawDebug.logLevel, DEFAULT_CONFIG.debug.logLevel),\n },\n };\n}\n\nexport async function readConfig(): Promise<KitConfig> {\n try {\n const content = await readFile(CONFIG_PATH, \"utf8\");\n const parsed = JSON.parse(content) as unknown;\n return sanitizeConfig(parsed);\n } catch {\n return sanitizeConfig(DEFAULT_CONFIG);\n }\n}\n\nasync function ensureConfigDirectory(): Promise<void> {\n await mkdir(path.dirname(CONFIG_PATH), { recursive: true });\n}\n\nexport async function writeConfig(config: KitConfig): Promise<void> {\n const safeConfig = sanitizeConfig(config);\n const serialized = `${JSON.stringify(safeConfig, null, 2)}\\n`;\n const tempPath = `${CONFIG_PATH}.tmp`;\n\n await ensureConfigDirectory();\n await writeFile(tempPath, serialized, \"utf8\");\n await rename(tempPath, CONFIG_PATH);\n}\n\nexport async function updateConfig(mutator: (current: KitConfig) => KitConfig): Promise<KitConfig> {\n const current = await readConfig();\n const next = sanitizeConfig(mutator(current));\n await writeConfig(next);\n return next;\n}\n\nexport function defaultConfig(): KitConfig {\n return sanitizeConfig(DEFAULT_CONFIG);\n}\n","import { appendFile, mkdir } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\n\nimport type { LogLevel } from \"../config\";\n\nconst LOG_DIR = path.join(homedir(), \".config\", \"opencode\", \"logs\");\nexport const LOG_PATH = path.join(LOG_DIR, \"opencode-kit.log\");\n\nconst LOG_WEIGHT: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport interface LogContext {\n [key: string]: unknown;\n}\n\nexport interface Logger {\n setLevel: (level: LogLevel) => void;\n debug: (event: string, message: string, context?: LogContext) => void;\n info: (event: string, message: string, context?: LogContext) => void;\n warn: (event: string, message: string, context?: LogContext) => void;\n error: (event: string, message: string, context?: LogContext) => void;\n}\n\ninterface LogLine {\n timestamp: string;\n level: LogLevel;\n event: string;\n message: string;\n context?: LogContext;\n}\n\nfunction shouldLog(minLevel: LogLevel, level: LogLevel): boolean {\n return LOG_WEIGHT[level] >= LOG_WEIGHT[minLevel];\n}\n\nasync function appendLogLine(line: LogLine): Promise<void> {\n try {\n await mkdir(LOG_DIR, { recursive: true });\n await appendFile(LOG_PATH, `${JSON.stringify(line)}\\n`, \"utf8\");\n } catch {\n // Best effort logging only.\n }\n}\n\nexport function createLogger(initialLevel: LogLevel = \"info\"): Logger {\n let minLevel = initialLevel;\n\n const write = (level: LogLevel, event: string, message: string, context?: LogContext): void => {\n if (!shouldLog(minLevel, level)) {\n return;\n }\n\n void appendLogLine({\n timestamp: new Date().toISOString(),\n level,\n event,\n message,\n context,\n });\n };\n\n return {\n setLevel: (level: LogLevel) => {\n minLevel = level;\n },\n debug: (event, message, context) => {\n write(\"debug\", event, message, context);\n },\n info: (event, message, context) => {\n write(\"info\", event, message, context);\n },\n warn: (event, message, context) => {\n write(\"warn\", event, message, context);\n },\n error: (event, message, context) => {\n write(\"error\", event, message, context);\n },\n };\n}\n","import { spawn } from \"node:child_process\";\n\nimport type { KitConfig } from \"../config\";\nimport type { Logger } from \"./logging\";\n\nconst FUNK_SOUND_PATH = \"/System/Library/Sounds/Funk.aiff\";\n\nfunction runCommand(command: string, args: string[], logger: Logger): Promise<void> {\n return new Promise((resolve) => {\n const child = spawn(command, args, {\n stdio: \"ignore\",\n });\n\n child.on(\"error\", (error) => {\n logger.warn(\"command.error\", \"Command failed to launch\", {\n command,\n args,\n error: error.message,\n });\n resolve();\n });\n\n child.on(\"exit\", (code) => {\n if (code !== 0) {\n logger.warn(\"command.nonzero\", \"Command exited non-zero\", {\n command,\n args,\n code,\n });\n }\n\n resolve();\n });\n });\n}\n\nfunction truncateSummary(text: string, maxChars: number): string {\n const trimmed = text.trim();\n if (trimmed.length <= maxChars) {\n return trimmed;\n }\n\n return `${trimmed.slice(0, maxChars - 3)}...`;\n}\n\nfunction cleanTextForSpeech(text: string): string {\n return text\n .replace(/```[\\s\\S]*?```/g, \" code block omitted \")\n .replace(/`([^`]+)`/g, \"$1\")\n .replace(/\\[(.*?)\\]\\((.*?)\\)/g, \"$1\")\n .replace(/[*_~#>]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nfunction shortSpeech(text: string, maxChars: number): string {\n const cleaned = cleanTextForSpeech(text);\n if (!cleaned) {\n return \"\";\n }\n\n if (cleaned.length <= maxChars) {\n return cleaned;\n }\n\n const sentence = cleaned.match(/(.+?[.!?])(\\s|$)/)?.[1]?.trim();\n if (sentence && sentence.length <= maxChars) {\n return sentence;\n }\n\n return truncateSummary(cleaned, maxChars);\n}\n\nfunction writeTerminalBell(logger: Logger): void {\n try {\n process.stdout.write(\"\\u0007\");\n } catch (error) {\n logger.warn(\"bell.error\", \"Failed to write terminal bell\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n}\n\nasync function speak(text: string, config: KitConfig, logger: Logger): Promise<void> {\n if (!config.speech.enabled) {\n return;\n }\n\n if (process.platform !== \"darwin\") {\n logger.info(\"speech.unsupported\", \"Speech skipped: platform unsupported\", {\n platform: process.platform,\n });\n return;\n }\n\n const clipped = shortSpeech(text, config.speech.maxChars);\n if (!clipped) {\n return;\n }\n\n const args: string[] = [];\n if (config.speech.voice) {\n args.push(\"-v\", config.speech.voice);\n }\n args.push(clipped);\n\n await runCommand(\"say\", args, logger);\n}\n\nasync function playErrorSound(config: KitConfig, logger: Logger): Promise<void> {\n if (!config.bells.enabled) {\n return;\n }\n\n if (process.platform !== \"darwin\") {\n writeTerminalBell(logger);\n return;\n }\n\n if (config.bells.errorSound === \"Funk\") {\n await runCommand(\"afplay\", [FUNK_SOUND_PATH], logger);\n }\n}\n\nexport async function notifyIdle(\n lastAssistantText: string,\n config: KitConfig,\n logger: Logger,\n): Promise<void> {\n if (config.bells.enabled) {\n writeTerminalBell(logger);\n }\n\n if (config.speech.enabled && lastAssistantText.trim()) {\n await speak(lastAssistantText, config, logger);\n }\n\n logger.debug(\"idle.notify\", \"Idle notification processed\", {\n bell: config.bells.enabled,\n speech: config.speech.enabled,\n });\n}\n\nexport async function notifyError(\n message: string,\n config: KitConfig,\n logger: Logger,\n): Promise<void> {\n await playErrorSound(config, logger);\n\n if (config.speech.enabled) {\n const safeMessage = message?.trim() || \"Unknown error\";\n await speak(`OpenCode error: ${safeMessage}`, config, logger);\n }\n\n logger.warn(\"error.notify\", \"Error notification processed\", {\n bell: config.bells.enabled,\n speech: config.speech.enabled,\n });\n}\n","import type { Plugin } from \"@opencode-ai/plugin\";\nimport type { Event } from \"@opencode-ai/sdk\";\n\nimport { readConfig } from \"./config\";\nimport { createLogger } from \"./features/logging\";\nimport { notifyError, notifyIdle } from \"./features/sounds\";\n\nfunction getErrorMessage(event: Event): string {\n if (event.type !== \"session.error\") {\n return \"Unknown error\";\n }\n\n const rawError = event.properties.error;\n if (!rawError) {\n return \"Agent encountered an error.\";\n }\n\n const errorData = rawError.data;\n if (errorData && typeof errorData === \"object\") {\n if (\n \"message\" in errorData &&\n typeof errorData.message === \"string\" &&\n errorData.message.trim()\n ) {\n return `Agent encountered an error: ${errorData.message.trim()}`;\n }\n }\n\n if (typeof rawError.name === \"string\" && rawError.name.trim()) {\n return `Agent encountered an error: ${rawError.name.trim()}`;\n }\n\n return \"Agent encountered an error.\";\n}\n\nexport const OpencodeKit: Plugin = async (input) => {\n const logger = createLogger();\n const latestAssistantMessageBySession = new Map<string, string>();\n const latestAssistantTextByMessage = new Map<string, string>();\n const lastSpokenMessageBySession = new Map<string, string>();\n\n return {\n event: async ({ event }): Promise<void> => {\n if (event.type === \"message.updated\") {\n const info = event.properties.info;\n if (info.role === \"assistant\") {\n latestAssistantMessageBySession.set(info.sessionID, info.id);\n }\n return;\n }\n\n if (event.type === \"message.part.updated\") {\n const part = event.properties.part;\n if (\n part.type === \"text\" &&\n typeof part.messageID === \"string\" &&\n typeof part.text === \"string\"\n ) {\n latestAssistantTextByMessage.set(part.messageID, part.text);\n }\n return;\n }\n\n if (event.type !== \"session.idle\" && event.type !== \"session.error\") {\n return;\n }\n\n const config = await readConfig();\n logger.setLevel(config.debug.logLevel);\n\n if (event.type === \"session.idle\") {\n const sessionID = event.properties.sessionID;\n const latestMessageID = latestAssistantMessageBySession.get(sessionID);\n const lastText = latestMessageID\n ? (latestAssistantTextByMessage.get(latestMessageID) ?? \"\")\n : \"\";\n const previouslySpoken = lastSpokenMessageBySession.get(sessionID);\n\n if (latestMessageID && previouslySpoken === latestMessageID) {\n logger.debug(\"idle.skip_duplicate\", \"Skipping duplicate idle speech\", {\n sessionID,\n messageID: latestMessageID,\n });\n return;\n }\n\n if (!latestMessageID || !lastText) {\n logger.warn(\"idle.no_summary\", \"No cached assistant summary for idle event\", {\n sessionID,\n hasMessageID: Boolean(latestMessageID),\n hasText: Boolean(lastText),\n });\n await notifyIdle(\"\", config, logger);\n return;\n }\n\n lastSpokenMessageBySession.set(sessionID, latestMessageID);\n await notifyIdle(lastText, config, logger);\n return;\n }\n\n await notifyError(getErrorMessage(event), config, logger);\n },\n };\n};\n\nexport default OpencodeKit;\n"],"mappings":"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","names":["mkdir","homedir","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@akonwi/opencode-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Personal OpenCode workflow plugin with bells, speech, and runtime toggles",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/plugin.js",
|
|
7
|
+
"types": "./dist/plugin.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/plugin.js",
|
|
11
|
+
"types": "./dist/plugin.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"oc-kit": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"lint": "biome check .",
|
|
22
|
+
"format": "biome format --write .",
|
|
23
|
+
"check": "bun run lint && bun run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["opencode", "plugin", "notifications", "speech"],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@opencode-ai/plugin": "^1.1.64"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^1.9.4",
|
|
35
|
+
"@types/node": "^22.13.4",
|
|
36
|
+
"tsup": "^8.4.0",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"packageManager": "bun@1.2.16"
|
|
43
|
+
}
|