oh-my-githubcopilot 1.8.1 → 2.0.0-alpha.1cd01f4
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/plugin.json +23 -3
- package/AGENTS.md +29 -25
- package/CHANGELOG.md +49 -336
- package/README.md +7 -2
- package/agents/code-reviewer.agent.md +5 -0
- package/agents/{simplifier.agent.md → code-simplifier.agent.md} +2 -2
- package/agents/debugger.agent.md +1 -1
- package/agents/document-specialist.agent.md +6 -1
- package/agents/{explorer.agent.md → explore.agent.md} +2 -2
- package/agents/security-reviewer.agent.md +1 -1
- package/agents/test-engineer.agent.md +3 -1
- package/bin/omp-statusline.mjs +12 -11
- package/bin/omp-statusline.mjs.map +2 -2
- package/bin/omp.mjs +885 -42
- package/bin/omp.mjs.map +4 -4
- package/dist/hooks/delegation-enforcer.mjs +65 -13
- package/dist/hooks/delegation-enforcer.mjs.map +4 -4
- package/dist/hooks/hud-emitter.mjs +80 -28
- package/dist/hooks/hud-emitter.mjs.map +4 -4
- package/dist/hooks/keyword-detector.mjs +117 -11
- package/dist/hooks/keyword-detector.mjs.map +4 -4
- package/dist/hooks/model-router.mjs +64 -12
- package/dist/hooks/model-router.mjs.map +4 -4
- package/dist/hooks/stop-continuation.mjs +66 -14
- package/dist/hooks/stop-continuation.mjs.map +4 -4
- package/dist/hooks/token-tracker.mjs +81 -19
- package/dist/hooks/token-tracker.mjs.map +4 -4
- package/dist/mcp/server.mjs +17 -16
- package/dist/mcp/server.mjs.map +2 -2
- package/extension/extension.mjs +659 -0
- package/hooks/hooks.json +6 -6
- package/package.json +3 -2
- package/plugin.json +23 -3
- package/skills/build-fix/SKILL.md +35 -0
- package/skills/cancel/SKILL.md +33 -0
- package/skills/ccg/SKILL.md +37 -0
- package/skills/code-review/SKILL.md +33 -0
- package/skills/deep-dive/SKILL.md +33 -0
- package/skills/deepinit/SKILL.md +33 -0
- package/skills/deepsearch/SKILL.md +33 -0
- package/skills/design/SKILL.md +37 -0
- package/skills/external-context/SKILL.md +33 -0
- package/skills/help/SKILL.md +33 -0
- package/skills/omp-doctor/SKILL.md +23 -1
- package/skills/omp-reference/SKILL.md +20 -24
- package/skills/remember/SKILL.md +39 -0
- package/skills/research/SKILL.md +1 -1
- package/skills/sciomc/SKILL.md +35 -0
- package/skills/security-review/SKILL.md +33 -0
- package/skills/self-improve/SKILL.md +35 -0
- package/skills/ultragoal/SKILL.md +33 -0
- package/skills/ultraqa/SKILL.md +33 -0
- package/skills/verify/SKILL.md +33 -0
- package/skills/visual-verdict/SKILL.md +35 -0
- package/skills/web-clone/SKILL.md +35 -0
- package/skills/writer-memory/SKILL.md +37 -0
- package/agents/orchestrator.agent.md +0 -26
- package/agents/researcher.agent.md +0 -18
- package/agents/reviewer.agent.md +0 -23
- package/agents/tester.agent.md +0 -20
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
// src/hooks/model-router.mts
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
+
|
|
4
|
+
// src/hooks/runner.mts
|
|
5
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
async function readStdin() {
|
|
9
|
+
const readStdinActual = async () => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
chunks.push(String(chunk));
|
|
13
|
+
}
|
|
14
|
+
return chunks.join("");
|
|
15
|
+
};
|
|
16
|
+
const stdinTimeout = new Promise(
|
|
17
|
+
(resolve) => setTimeout(
|
|
18
|
+
() => resolve(""),
|
|
19
|
+
parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
return Promise.race([readStdinActual(), stdinTimeout]);
|
|
23
|
+
}
|
|
24
|
+
function logHookFailure(hook, reason) {
|
|
25
|
+
try {
|
|
26
|
+
process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
|
|
27
|
+
`);
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const logsDir = join(homedir(), ".omp", "logs");
|
|
32
|
+
mkdirSync(logsDir, { recursive: true });
|
|
33
|
+
const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
|
|
34
|
+
appendFileSync(join(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function runHookMain(processHook2, options = {}) {
|
|
39
|
+
let outputJson;
|
|
40
|
+
try {
|
|
41
|
+
const input = JSON.parse(await readStdin());
|
|
42
|
+
const serialized = JSON.stringify(processHook2(input));
|
|
43
|
+
if (typeof serialized !== "string") {
|
|
44
|
+
throw new Error("hook produced no serializable output");
|
|
45
|
+
}
|
|
46
|
+
outputJson = serialized;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
49
|
+
logHookFailure(options.hookName ?? "unknown", reason);
|
|
50
|
+
const failOpen = {
|
|
51
|
+
...options.failOpenDecision ? { decision: "allow" } : {},
|
|
52
|
+
status: "error",
|
|
53
|
+
latencyMs: 0,
|
|
54
|
+
mutations: [],
|
|
55
|
+
log: [`fail-open: ${reason}`]
|
|
56
|
+
};
|
|
57
|
+
outputJson = JSON.stringify(failOpen);
|
|
58
|
+
}
|
|
59
|
+
console.log(outputJson);
|
|
60
|
+
process.exitCode = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/hooks/model-router.mts
|
|
3
64
|
var TIER_RECOMMENDATIONS = {
|
|
4
65
|
high: "model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)",
|
|
5
66
|
standard: "model: claude-sonnet-4.6 recommended for this task (standard implementation and review)",
|
|
@@ -48,10 +109,10 @@ function processHook(input) {
|
|
|
48
109
|
}
|
|
49
110
|
}
|
|
50
111
|
function getAgentTier(agentId) {
|
|
51
|
-
if (["orchestrator", "architect", "planner", "reviewer
|
|
112
|
+
if (["orchestrator", "architect", "planner", "security-reviewer", "critic", "debugger", "code-reviewer", "analyst", "designer", "code-simplifier"].includes(agentId)) {
|
|
52
113
|
return "high";
|
|
53
114
|
}
|
|
54
|
-
if (["
|
|
115
|
+
if (["explore"].includes(agentId)) {
|
|
55
116
|
return "fast";
|
|
56
117
|
}
|
|
57
118
|
return "standard";
|
|
@@ -62,16 +123,7 @@ function agentTierToModel(tier) {
|
|
|
62
123
|
return "sonnet";
|
|
63
124
|
}
|
|
64
125
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
65
|
-
|
|
66
|
-
const output = processHook(input);
|
|
67
|
-
console.log(JSON.stringify(output));
|
|
68
|
-
}
|
|
69
|
-
async function readStdin() {
|
|
70
|
-
const chunks = [];
|
|
71
|
-
for await (const chunk of process.stdin) {
|
|
72
|
-
chunks.push(chunk);
|
|
73
|
-
}
|
|
74
|
-
return chunks.join("");
|
|
126
|
+
await runHookMain(processHook, { failOpenDecision: true, hookName: "model-router" });
|
|
75
127
|
}
|
|
76
128
|
export {
|
|
77
129
|
processHook
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/hooks/model-router.mts"],
|
|
4
|
-
"sourcesContent": ["/**\n * model-router hook\n * Trigger: pre-cycle (PreToolUse equivalent)\n * Priority: 80\n *\n * Reads agent frontmatter model_tier and adds advisory additionalContext.\n */\n\nexport interface HookInput {\n hook_type: \"PreToolUse\";\n tool_name?: string;\n agent_id?: string;\n session_id?: string;\n}\n\nexport interface HookOutput {\n decision?: \"allow\";\n additionalContext?: string;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_model\"; model: \"opus\" | \"sonnet\" | \"haiku\" }>;\n log: string[];\n}\n\n// Model tier recommendations \u2014 advisory only\nconst TIER_RECOMMENDATIONS: Record<string, string> = {\n high: \"model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)\",\n standard: \"model: claude-sonnet-4.6 recommended for this task (standard implementation and review)\",\n fast: \"model: gpt-5.4-mini or haiku recommended for quick lookups and formatting\",\n};\n\n// Default if agent tier unknown\nconst DEFAULT_TIER = \"standard\";\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n\n try {\n if (input.hook_type !== \"PreToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const agentId = input.agent_id;\n if (!agentId) {\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Agent tier is determined by agent frontmatter in the agent definition files.\n // This hook reads agent metadata from the session state or agent registry.\n // For now, we use a simple mapping based on known agent tiers.\n const agentTier = getAgentTier(agentId);\n const recommendation = TIER_RECOMMENDATIONS[agentTier] || TIER_RECOMMENDATIONS[DEFAULT_TIER];\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_model\", model: agentTierToModel(agentTier) },\n ];\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n additionalContext: recommendation,\n mutations,\n log: [`${agentId} \u2192 tier: ${agentTier} \u2192 ${agentTierToModel(agentTier)}`],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\nfunction getAgentTier(agentId: string): string {\n // Tier 1 \u2014 High\n if ([\"orchestrator\", \"architect\", \"planner\", \"reviewer
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": []
|
|
3
|
+
"sources": ["../../src/hooks/model-router.mts", "../../src/hooks/runner.mts"],
|
|
4
|
+
"sourcesContent": ["/**\n * model-router hook\n * Trigger: pre-cycle (PreToolUse equivalent)\n * Priority: 80\n *\n * Reads agent frontmatter model_tier and adds advisory additionalContext.\n */\n\nexport interface HookInput {\n hook_type: \"PreToolUse\";\n tool_name?: string;\n agent_id?: string;\n session_id?: string;\n}\n\nexport interface HookOutput {\n decision?: \"allow\";\n additionalContext?: string;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_model\"; model: \"opus\" | \"sonnet\" | \"haiku\" }>;\n log: string[];\n}\n\n// Model tier recommendations \u2014 advisory only\nconst TIER_RECOMMENDATIONS: Record<string, string> = {\n high: \"model: claude-opus-4.6 or gpt-5 recommended for this task (architecture, security, critical decisions)\",\n standard: \"model: claude-sonnet-4.6 recommended for this task (standard implementation and review)\",\n fast: \"model: gpt-5.4-mini or haiku recommended for quick lookups and formatting\",\n};\n\n// Default if agent tier unknown\nconst DEFAULT_TIER = \"standard\";\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n\n try {\n if (input.hook_type !== \"PreToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const agentId = input.agent_id;\n if (!agentId) {\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Agent tier is determined by agent frontmatter in the agent definition files.\n // This hook reads agent metadata from the session state or agent registry.\n // For now, we use a simple mapping based on known agent tiers.\n const agentTier = getAgentTier(agentId);\n const recommendation = TIER_RECOMMENDATIONS[agentTier] || TIER_RECOMMENDATIONS[DEFAULT_TIER];\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_model\", model: agentTierToModel(agentTier) },\n ];\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n additionalContext: recommendation,\n mutations,\n log: [`${agentId} \u2192 tier: ${agentTier} \u2192 ${agentTierToModel(agentTier)}`],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\nfunction getAgentTier(agentId: string): string {\n // Tier 1 \u2014 High (\"orchestrator\" is the top-level coordinator role, not a delegatable agent)\n if ([\"orchestrator\", \"architect\", \"planner\", \"security-reviewer\", \"critic\", \"debugger\", \"code-reviewer\", \"analyst\", \"designer\", \"code-simplifier\"].includes(agentId)) {\n return \"high\";\n }\n // Tier 3 \u2014 Fast\n if ([\"explore\"].includes(agentId)) {\n return \"fast\";\n }\n // Tier 2 \u2014 Standard (default)\n return \"standard\";\n}\n\nfunction agentTierToModel(tier: string): \"opus\" | \"sonnet\" | \"haiku\" {\n if (tier === \"high\") return \"opus\";\n if (tier === \"fast\") return \"haiku\";\n return \"sonnet\";\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { failOpenDecision: true, hookName: \"model-router\" });\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["processHook"]
|
|
7
7
|
}
|
|
@@ -1,14 +1,75 @@
|
|
|
1
1
|
// src/hooks/stop-continuation.mts
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
|
+
import { homedir as homedir2 } from "os";
|
|
4
|
+
import { join as join2 } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/hooks/runner.mts
|
|
8
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
3
9
|
import { homedir } from "os";
|
|
4
10
|
import { join } from "path";
|
|
5
|
-
|
|
11
|
+
async function readStdin() {
|
|
12
|
+
const readStdinActual = async () => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of process.stdin) {
|
|
15
|
+
chunks.push(String(chunk));
|
|
16
|
+
}
|
|
17
|
+
return chunks.join("");
|
|
18
|
+
};
|
|
19
|
+
const stdinTimeout = new Promise(
|
|
20
|
+
(resolve) => setTimeout(
|
|
21
|
+
() => resolve(""),
|
|
22
|
+
parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
return Promise.race([readStdinActual(), stdinTimeout]);
|
|
26
|
+
}
|
|
27
|
+
function logHookFailure(hook, reason) {
|
|
28
|
+
try {
|
|
29
|
+
process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
|
|
30
|
+
`);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const logsDir = join(homedir(), ".omp", "logs");
|
|
35
|
+
mkdirSync(logsDir, { recursive: true });
|
|
36
|
+
const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
|
|
37
|
+
appendFileSync(join(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function runHookMain(processHook2, options = {}) {
|
|
42
|
+
let outputJson;
|
|
43
|
+
try {
|
|
44
|
+
const input = JSON.parse(await readStdin());
|
|
45
|
+
const serialized = JSON.stringify(processHook2(input));
|
|
46
|
+
if (typeof serialized !== "string") {
|
|
47
|
+
throw new Error("hook produced no serializable output");
|
|
48
|
+
}
|
|
49
|
+
outputJson = serialized;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
52
|
+
logHookFailure(options.hookName ?? "unknown", reason);
|
|
53
|
+
const failOpen = {
|
|
54
|
+
...options.failOpenDecision ? { decision: "allow" } : {},
|
|
55
|
+
status: "error",
|
|
56
|
+
latencyMs: 0,
|
|
57
|
+
mutations: [],
|
|
58
|
+
log: [`fail-open: ${reason}`]
|
|
59
|
+
};
|
|
60
|
+
outputJson = JSON.stringify(failOpen);
|
|
61
|
+
}
|
|
62
|
+
console.log(outputJson);
|
|
63
|
+
process.exitCode = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/hooks/stop-continuation.mts
|
|
6
67
|
function getModeStatePath(mode, sessionId) {
|
|
7
|
-
const base =
|
|
68
|
+
const base = join2(homedir2(), ".omp", "state");
|
|
8
69
|
if (sessionId) {
|
|
9
|
-
return
|
|
70
|
+
return join2(base, "sessions", sessionId, `${mode}-state.json`);
|
|
10
71
|
}
|
|
11
|
-
return
|
|
72
|
+
return join2(base, `${mode}-state.json`);
|
|
12
73
|
}
|
|
13
74
|
function readModeState(mode, sessionId) {
|
|
14
75
|
try {
|
|
@@ -66,16 +127,7 @@ function processHook(input) {
|
|
|
66
127
|
}
|
|
67
128
|
}
|
|
68
129
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
69
|
-
|
|
70
|
-
const output = processHook(input);
|
|
71
|
-
console.log(JSON.stringify(output));
|
|
72
|
-
}
|
|
73
|
-
async function readStdin() {
|
|
74
|
-
const chunks = [];
|
|
75
|
-
for await (const chunk of process.stdin) {
|
|
76
|
-
chunks.push(chunk);
|
|
77
|
-
}
|
|
78
|
-
return chunks.join("");
|
|
130
|
+
await runHookMain(processHook, { hookName: "stop-continuation" });
|
|
79
131
|
}
|
|
80
132
|
export {
|
|
81
133
|
processHook
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/hooks/stop-continuation.mts"],
|
|
4
|
-
"sourcesContent": ["/**\n * stop-continuation hook\n * Trigger: post-message (SessionEnd equivalent)\n * Priority: 50\n *\n * Detects active persistent modes (ralph, ultrawork, team) and\n * returns continue instructions so the user can decide whether\n * to keep going.\n */\n\nimport { readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface HookInput {\n hook_type: \"SessionEnd\";\n session_id?: string;\n message?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"stop\"; reason: string } | { type: \"log\"; level: \"info\"; message: string }>;\n log: string[];\n}\n\ninterface ModeState {\n active?: boolean;\n mode?: string;\n linked_ultrawork?: boolean;\n linked_team?: boolean;\n}\n\nfunction getModeStatePath(mode: string, sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, `${mode}-state.json`);\n }\n return join(base, `${mode}-state.json`);\n}\n\nfunction readModeState(mode: string, sessionId?: string): ModeState | null {\n try {\n const path = getModeStatePath(mode, sessionId);\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n// Priority order for checking: team > ralph > ultrawork\nconst PERSISTENT_MODES = [\"team\", \"ralph\", \"ultrawork\"];\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"SessionEnd\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Check for active persistent modes\n for (const mode of PERSISTENT_MODES) {\n const state = readModeState(mode, input.session_id);\n if (state?.active) {\n const reason = `${mode} mode is still active.`;\n log.push(`Stop continuation: ${reason}`);\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [\n {\n type: \"stop\",\n reason: `${reason} Use /cancel to end it, or continue the session to keep going.`,\n },\n { type: \"log\", level: \"info\", message: reason },\n ],\n log,\n };\n }\n }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [\"No persistent modes active\"],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported)
|
|
5
|
-
"mappings": ";AAUA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,YAAY;
|
|
6
|
-
"names": []
|
|
3
|
+
"sources": ["../../src/hooks/stop-continuation.mts", "../../src/hooks/runner.mts"],
|
|
4
|
+
"sourcesContent": ["/**\n * stop-continuation hook\n * Trigger: post-message (SessionEnd equivalent)\n * Priority: 50\n *\n * Detects active persistent modes (ralph, ultrawork, team) and\n * returns continue instructions so the user can decide whether\n * to keep going.\n */\n\nimport { readFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface HookInput {\n hook_type: \"SessionEnd\";\n session_id?: string;\n message?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"stop\"; reason: string } | { type: \"log\"; level: \"info\"; message: string }>;\n log: string[];\n}\n\ninterface ModeState {\n active?: boolean;\n mode?: string;\n linked_ultrawork?: boolean;\n linked_team?: boolean;\n}\n\nfunction getModeStatePath(mode: string, sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, `${mode}-state.json`);\n }\n return join(base, `${mode}-state.json`);\n}\n\nfunction readModeState(mode: string, sessionId?: string): ModeState | null {\n try {\n const path = getModeStatePath(mode, sessionId);\n return JSON.parse(readFileSync(path, \"utf-8\"));\n } catch {\n return null;\n }\n}\n\n// Priority order for checking: team > ralph > ultrawork\nconst PERSISTENT_MODES = [\"team\", \"ralph\", \"ultrawork\"];\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"SessionEnd\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n // Check for active persistent modes\n for (const mode of PERSISTENT_MODES) {\n const state = readModeState(mode, input.session_id);\n if (state?.active) {\n const reason = `${mode} mode is still active.`;\n log.push(`Stop continuation: ${reason}`);\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [\n {\n type: \"stop\",\n reason: `${reason} Use /cancel to end it, or continue the session to keep going.`,\n },\n { type: \"log\", level: \"info\", message: reason },\n ],\n log,\n };\n }\n }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [\"No persistent modes active\"],\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { hookName: \"stop-continuation\" });\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
|
|
5
|
+
"mappings": ";AAUA,SAAS,oBAAoB;AAC7B,SAAS,WAAAA,gBAAe;AACxB,SAAS,QAAAC,aAAY;AAiGrB,SAAS,qBAAqB;;;AC/F9B,SAAS,gBAAgB,iBAAiB;AAC1C,SAAS,eAAe;AACxB,SAAS,YAAY;AAqBrB,eAAsB,YAA6B;AACjD,QAAM,kBAAkB,YAA6B;AACnD,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,OAAO,KAAK,CAAC;AAAA,IAC3B;AACA,WAAO,OAAO,KAAK,EAAE;AAAA,EACvB;AAEA,QAAM,eAAe,IAAI;AAAA,IAAgB,CAAC,YACxC;AAAA,MACE,MAAM,QAAQ,EAAE;AAAA,MACf,SAAS,QAAQ,IAAI,6BAA6B,KAAK,KAAK;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO,QAAQ,KAAK,CAAC,gBAAgB,GAAG,YAAY,CAAC;AACvD;AAOA,SAAS,eAAe,MAAc,QAAsB;AAC1D,MAAI;AACF,YAAQ,OAAO,MAAM,wBAAwB,IAAI,KAAK,MAAM;AAAA,CAAI;AAAA,EAClE,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,UAAU,KAAK,QAAQ,GAAG,QAAQ,MAAM;AAC9C,cAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACtC,UAAM,SAAS,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,MAAM,OAAO,CAAC;AAC5E,mBAAe,KAAK,SAAS,qBAAqB,GAAG,SAAS,MAAM,OAAO;AAAA,EAC7E,QAAQ;AAAA,EAER;AACF;AAOA,eAAsB,YACpBC,cACA,UAA0B,CAAC,GACZ;AACf,MAAI;AACJ,MAAI;AACF,UAAM,QAAQ,KAAK,MAAM,MAAM,UAAU,CAAC;AAC1C,UAAM,aAAa,KAAK,UAAUA,aAAY,KAAK,CAAC;AACpD,QAAI,OAAO,eAAe,UAAU;AAClC,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AACA,iBAAa;AAAA,EACf,SAAS,KAAK;AACZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,mBAAe,QAAQ,YAAY,WAAW,MAAM;AACpD,UAAM,WAA2B;AAAA,MAC/B,GAAI,QAAQ,mBAAmB,EAAE,UAAU,QAAiB,IAAI,CAAC;AAAA,MACjE,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,cAAc,MAAM,EAAE;AAAA,IAC9B;AACA,iBAAa,KAAK,UAAU,QAAQ;AAAA,EACtC;AACA,UAAQ,IAAI,UAAU;AACtB,UAAQ,WAAW;AACrB;;;ADzEA,SAAS,iBAAiB,MAAc,WAA4B;AAClE,QAAM,OAAOC,MAAKC,SAAQ,GAAG,QAAQ,OAAO;AAC5C,MAAI,WAAW;AACb,WAAOD,MAAK,MAAM,YAAY,WAAW,GAAG,IAAI,aAAa;AAAA,EAC/D;AACA,SAAOA,MAAK,MAAM,GAAG,IAAI,aAAa;AACxC;AAEA,SAAS,cAAc,MAAc,WAAsC;AACzE,MAAI;AACF,UAAM,OAAO,iBAAiB,MAAM,SAAS;AAC7C,WAAO,KAAK,MAAM,aAAa,MAAM,OAAO,CAAC;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,mBAAmB,CAAC,QAAQ,SAAS,WAAW;AAE/C,SAAS,YAAY,OAA8B;AACxD,QAAM,QAAQ,KAAK,IAAI;AACvB,QAAM,MAAgB,CAAC;AAEvB,MAAI;AACF,QAAI,MAAM,cAAc,cAAc;AACpC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,KAAK,IAAI,IAAI;AAAA,QACxB,WAAW,CAAC;AAAA,QACZ,KAAK,CAAC;AAAA,MACR;AAAA,IACF;AAGA,eAAW,QAAQ,kBAAkB;AACnC,YAAM,QAAQ,cAAc,MAAM,MAAM,UAAU;AAClD,UAAI,OAAO,QAAQ;AACjB,cAAM,SAAS,GAAG,IAAI;AACtB,YAAI,KAAK,sBAAsB,MAAM,EAAE;AAEvC,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,WAAW,KAAK,IAAI,IAAI;AAAA,UACxB,WAAW;AAAA,YACT;AAAA,cACE,MAAM;AAAA,cACN,QAAQ,GAAG,MAAM;AAAA,YACnB;AAAA,YACA,EAAE,MAAM,OAAO,OAAO,QAAQ,SAAS,OAAO;AAAA,UAChD;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,4BAA4B;AAAA,IACpC;AAAA,EACF,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK,IAAI,IAAI;AAAA,MACxB,WAAW,CAAC;AAAA,MACZ,KAAK,CAAC,UAAU,GAAG,EAAE;AAAA,IACvB;AAAA,EACF;AACF;AAOA,IAAI,QAAQ,KAAK,CAAC,MAAM,cAAc,YAAY,GAAG,GAAG;AACtD,QAAM,YAAY,aAAa,EAAE,UAAU,oBAAoB,CAAC;AAClE;",
|
|
6
|
+
"names": ["homedir", "join", "processHook", "join", "homedir"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/hooks/token-tracker.mts
|
|
2
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as
|
|
3
|
-
import { homedir as
|
|
4
|
-
import { join as
|
|
2
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3
|
+
import { homedir as homedir3 } from "os";
|
|
4
|
+
import { join as join3 } from "path";
|
|
5
5
|
|
|
6
6
|
// src/spending/tracker.mts
|
|
7
7
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -64,6 +64,67 @@ function incrementSpending(sessionId) {
|
|
|
64
64
|
|
|
65
65
|
// src/hooks/token-tracker.mts
|
|
66
66
|
import { fileURLToPath } from "url";
|
|
67
|
+
|
|
68
|
+
// src/hooks/runner.mts
|
|
69
|
+
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
70
|
+
import { homedir as homedir2 } from "os";
|
|
71
|
+
import { join as join2 } from "path";
|
|
72
|
+
async function readStdin() {
|
|
73
|
+
const readStdinActual = async () => {
|
|
74
|
+
const chunks = [];
|
|
75
|
+
for await (const chunk of process.stdin) {
|
|
76
|
+
chunks.push(String(chunk));
|
|
77
|
+
}
|
|
78
|
+
return chunks.join("");
|
|
79
|
+
};
|
|
80
|
+
const stdinTimeout = new Promise(
|
|
81
|
+
(resolve) => setTimeout(
|
|
82
|
+
() => resolve(""),
|
|
83
|
+
parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? "500") || 500
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
return Promise.race([readStdinActual(), stdinTimeout]);
|
|
87
|
+
}
|
|
88
|
+
function logHookFailure(hook, reason) {
|
|
89
|
+
try {
|
|
90
|
+
process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}
|
|
91
|
+
`);
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const logsDir = join2(homedir2(), ".omp", "logs");
|
|
96
|
+
mkdirSync2(logsDir, { recursive: true });
|
|
97
|
+
const record = JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), hook, reason });
|
|
98
|
+
appendFileSync(join2(logsDir, "hook-failures.jsonl"), record + "\n", "utf-8");
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function runHookMain(processHook2, options = {}) {
|
|
103
|
+
let outputJson;
|
|
104
|
+
try {
|
|
105
|
+
const input = JSON.parse(await readStdin());
|
|
106
|
+
const serialized = JSON.stringify(processHook2(input));
|
|
107
|
+
if (typeof serialized !== "string") {
|
|
108
|
+
throw new Error("hook produced no serializable output");
|
|
109
|
+
}
|
|
110
|
+
outputJson = serialized;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
113
|
+
logHookFailure(options.hookName ?? "unknown", reason);
|
|
114
|
+
const failOpen = {
|
|
115
|
+
...options.failOpenDecision ? { decision: "allow" } : {},
|
|
116
|
+
status: "error",
|
|
117
|
+
latencyMs: 0,
|
|
118
|
+
mutations: [],
|
|
119
|
+
log: [`fail-open: ${reason}`]
|
|
120
|
+
};
|
|
121
|
+
outputJson = JSON.stringify(failOpen);
|
|
122
|
+
}
|
|
123
|
+
console.log(outputJson);
|
|
124
|
+
process.exitCode = 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/hooks/token-tracker.mts
|
|
67
128
|
var MODEL_CONTEXTS = {
|
|
68
129
|
"claude-sonnet-4.5": 2e5,
|
|
69
130
|
"claude-sonnet-4": 2e5,
|
|
@@ -85,14 +146,14 @@ function estimateTokens(input) {
|
|
|
85
146
|
}
|
|
86
147
|
}
|
|
87
148
|
function getStatePath(sessionId) {
|
|
88
|
-
const base =
|
|
149
|
+
const base = join3(homedir3(), ".omp", "state");
|
|
89
150
|
if (sessionId) {
|
|
90
|
-
return
|
|
151
|
+
return join3(base, "sessions", sessionId, "session.json");
|
|
91
152
|
}
|
|
92
|
-
return
|
|
153
|
+
return join3(base, "session.json");
|
|
93
154
|
}
|
|
94
155
|
function ensureDir(path) {
|
|
95
|
-
|
|
156
|
+
mkdirSync3(path.substring(0, path.lastIndexOf("/")), { recursive: true });
|
|
96
157
|
}
|
|
97
158
|
function processHook(input) {
|
|
98
159
|
const start = Date.now();
|
|
@@ -109,7 +170,13 @@ function processHook(input) {
|
|
|
109
170
|
const statePath = getStatePath(input.session_id);
|
|
110
171
|
let state;
|
|
111
172
|
try {
|
|
112
|
-
|
|
173
|
+
const raw = JSON.parse(readFileSync2(statePath, "utf-8"));
|
|
174
|
+
state = {
|
|
175
|
+
...raw,
|
|
176
|
+
warnings_issued: new Set(
|
|
177
|
+
Array.isArray(raw.warnings_issued) ? raw.warnings_issued : []
|
|
178
|
+
)
|
|
179
|
+
};
|
|
113
180
|
} catch {
|
|
114
181
|
const fallbackModel = input.model ?? "default";
|
|
115
182
|
state = {
|
|
@@ -138,7 +205,11 @@ function processHook(input) {
|
|
|
138
205
|
}
|
|
139
206
|
try {
|
|
140
207
|
ensureDir(statePath);
|
|
141
|
-
writeFileSync2(
|
|
208
|
+
writeFileSync2(
|
|
209
|
+
statePath,
|
|
210
|
+
JSON.stringify({ ...state, warnings_issued: Array.from(state.warnings_issued) }),
|
|
211
|
+
"utf-8"
|
|
212
|
+
);
|
|
142
213
|
} catch (e) {
|
|
143
214
|
log.push(`Failed to write state: ${e}`);
|
|
144
215
|
}
|
|
@@ -163,16 +234,7 @@ function processHook(input) {
|
|
|
163
234
|
}
|
|
164
235
|
}
|
|
165
236
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
166
|
-
|
|
167
|
-
const output = processHook(input);
|
|
168
|
-
console.log(JSON.stringify(output));
|
|
169
|
-
}
|
|
170
|
-
async function readStdin() {
|
|
171
|
-
const chunks = [];
|
|
172
|
-
for await (const chunk of process.stdin) {
|
|
173
|
-
chunks.push(chunk);
|
|
174
|
-
}
|
|
175
|
-
return chunks.join("");
|
|
237
|
+
await runHookMain(processHook, { hookName: "token-tracker" });
|
|
176
238
|
}
|
|
177
239
|
export {
|
|
178
240
|
MODEL_CONTEXTS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/hooks/token-tracker.mts", "../../src/spending/tracker.mts"],
|
|
4
|
-
"sourcesContent": ["/**\n * token-tracker hook\n * Trigger: post-message (PostToolUse equivalent)\n * Priority: 70\n *\n * Estimates token usage from character counts (1 token \u2248 4 chars).\n * Accumulates in session state. Warns at 60%, 80%, 90% thresholds.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { incrementSpending } from \"../spending/tracker.mjs\";\n\nexport interface HookInput {\n hook_type: \"PostToolUse\";\n tool_name?: string;\n tool_input?: unknown;\n tool_output?: unknown;\n session_id?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_token_budget\"; budget: number } | { type: \"emit_hud\"; hudEmit: unknown } | { type: \"log\"; level: \"info\" | \"warn\" | \"error\"; message: string }>;\n log: string[];\n}\n\ninterface SessionState {\n tokens_estimated: number;\n token_budget: number;\n context_pct: number;\n warnings_issued: Set<string>;\n}\n\n// Model context windows in tokens (for future model-specific budget lookup)\n// Exported for potential external use\nexport const MODEL_CONTEXTS = {\n \"claude-sonnet-4.5\": 200_000,\n \"claude-sonnet-4\": 200_000,\n \"claude-sonnet-4.6\": 200_000,\n \"claude-opus-4.6\": 200_000,\n \"gpt-5\": 128_000,\n \"gpt-5.4-mini\": 128_000,\n \"gemini-3-pro\": 128_000,\n default: 200_000,\n};\n\nconst WARNING_THRESHOLDS = [60, 80, 90];\n\nexport function estimateTokens(input: unknown): number {\n if (!input) return 0;\n try {\n const str = typeof input === \"string\" ? input : JSON.stringify(input);\n return Math.ceil(str.length / 4);\n } catch {\n return 0;\n }\n}\n\nfunction getStatePath(sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, \"session.json\");\n }\n return join(base, \"session.json\");\n}\n\nfunction ensureDir(path: string): void {\n mkdirSync(path.substring(0, path.lastIndexOf(\"/\")), { recursive: true });\n}\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"PostToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const statePath = getStatePath(input.session_id);\n let state: SessionState;\n\n try {\n state = JSON.parse(readFileSync(statePath, \"utf-8\"));\n } catch {\n // Initialize state if not found \u2014 budget derived from model when available\n const fallbackModel = (input as { model?: string }).model ?? \"default\";\n state = {\n tokens_estimated: 0,\n token_budget: (MODEL_CONTEXTS as Record<string, number>)[fallbackModel] ?? MODEL_CONTEXTS[\"default\"] ?? 200_000,\n context_pct: 0,\n warnings_issued: new Set(),\n };\n }\n\n const inputTokens = estimateTokens(input.tool_input);\n const outputTokens = estimateTokens(input.tool_output);\n const delta = inputTokens + outputTokens;\n\n state.tokens_estimated += delta;\n state.context_pct = Math.min(100, Math.round((state.tokens_estimated / state.token_budget) * 100));\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_token_budget\", budget: state.token_budget },\n ];\n\n // Check warning thresholds\n for (const threshold of WARNING_THRESHOLDS) {\n const key = `warn_${threshold}`;\n if (state.context_pct >= threshold && !state.warnings_issued.has(key)) {\n state.warnings_issued.add(key);\n const message =\n threshold >= 90\n ? `CRITICAL: Context at ${state.context_pct}%. Tokens near budget limit.`\n : threshold >= 80\n ? `WARNING: Context at ${state.context_pct}%. Consider enabling ecomode.`\n : `INFO: Context at ${state.context_pct}%.`;\n mutations.push({ type: \"log\", level: threshold >= 80 ? \"warn\" : \"info\", message });\n log.push(message);\n }\n }\n\n // Write state back\n try {\n ensureDir(statePath);\n writeFileSync(statePath, JSON.stringify(state), \"utf-8\");\n } catch (e) {\n log.push(`Failed to write state: ${e}`);\n }\n\n // Track premium request spending\n const sessionId = input.session_id ?? `omp-${Date.now()}`;\n try { incrementSpending(sessionId); } catch { /* non-blocking */ }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations,\n log,\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported)\nimport { fileURLToPath } from \"url\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n const input: HookInput = JSON.parse(await readStdin());\n const output = processHook(input);\n console.log(JSON.stringify(output));\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n return chunks.join(\"\");\n}\n", "/**\n * Spending tracker for OMP.\n * Tracks premium API requests per session and per calendar month.\n * Persists to ~/.omp/state/spending-monthly.json\n *\n * // v1.1 known limitation: no /omp:spending reset command. To reset monthly counter manually: rm ~/.omp/state/spending-monthly.json\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join, dirname } from \"path\";\nimport type { SpendingState } from \"./types.mjs\";\n\nconst SPENDING_PATH = join(homedir(), \".omp\", \"state\", \"spending-monthly.json\");\n\nfunction currentMonth(): string {\n const now = new Date();\n return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n}\n\nexport function loadSpending(sessionId: string): SpendingState {\n let raw: SpendingState;\n try {\n raw = JSON.parse(readFileSync(SPENDING_PATH, \"utf-8\")) as SpendingState;\n } catch {\n // Missing or malformed file \u2014 start fresh\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month: currentMonth(),\n monthlyPremiumRequests: 0,\n };\n }\n\n const month = currentMonth();\n\n // Reset monthly counter when month rolls over\n if (raw.month !== month) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: 0,\n };\n }\n\n // Reset session counter when session changes\n if (raw.sessionId !== sessionId) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: raw.monthlyPremiumRequests,\n };\n }\n\n return { ...raw, version: 1 };\n}\n\nexport function saveSpending(state: SpendingState): void {\n try {\n mkdirSync(dirname(SPENDING_PATH), { recursive: true });\n writeFileSync(SPENDING_PATH, JSON.stringify(state, null, 2), \"utf-8\");\n } catch (e) {\n console.warn(`[OMP] spending: failed to save state: ${e}`);\n }\n}\n\nexport function incrementSpending(sessionId: string): SpendingState {\n const state = loadSpending(sessionId);\n state.sessionPremiumRequests += 1;\n state.monthlyPremiumRequests += 1;\n saveSpending(state);\n return state;\n}\n"],
|
|
5
|
-
"mappings": ";AASA,SAAS,gBAAAA,eAAc,iBAAAC,gBAAe,aAAAC,kBAAiB;AACvD,SAAS,WAAAC,gBAAe;AACxB,SAAS,QAAAC,aAAY;;;ACHrB,SAAS,cAAc,eAAe,iBAAiB;AACvD,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAG9B,IAAM,gBAAgB,KAAK,QAAQ,GAAG,QAAQ,SAAS,uBAAuB;AAE9E,SAAS,eAAuB;AAC9B,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO,GAAG,IAAI,YAAY,CAAC,IAAI,OAAO,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC;AAC5E;AAEO,SAAS,aAAa,WAAkC;AAC7D,MAAI;AACJ,MAAI;AACF,UAAM,KAAK,MAAM,aAAa,eAAe,OAAO,CAAC;AAAA,EACvD,QAAQ;AAEN,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,wBAAwB;AAAA,MACxB,OAAO,aAAa;AAAA,MACpB,wBAAwB;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,QAAQ,aAAa;AAG3B,MAAI,IAAI,UAAU,OAAO;AACvB,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,wBAAwB;AAAA,MACxB;AAAA,MACA,wBAAwB;AAAA,IAC1B;AAAA,EACF;AAGA,MAAI,IAAI,cAAc,WAAW;AAC/B,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,MACA,wBAAwB;AAAA,MACxB;AAAA,MACA,wBAAwB,IAAI;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,GAAG,KAAK,SAAS,EAAE;AAC9B;AAEO,SAAS,aAAa,OAA4B;AACvD,MAAI;AACF,cAAU,QAAQ,aAAa,GAAG,EAAE,WAAW,KAAK,CAAC;AACrD,kBAAc,eAAe,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EACtE,SAAS,GAAG;AACV,YAAQ,KAAK,yCAAyC,CAAC,EAAE;AAAA,EAC3D;AACF;AAEO,SAAS,kBAAkB,WAAkC;AAClE,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,0BAA0B;AAChC,QAAM,0BAA0B;AAChC,eAAa,KAAK;AAClB,SAAO;AACT;;;
|
|
6
|
-
"names": ["readFileSync", "writeFileSync", "mkdirSync", "homedir", "join", "join", "homedir", "mkdirSync", "readFileSync", "writeFileSync"]
|
|
3
|
+
"sources": ["../../src/hooks/token-tracker.mts", "../../src/spending/tracker.mts", "../../src/hooks/runner.mts"],
|
|
4
|
+
"sourcesContent": ["/**\n * token-tracker hook\n * Trigger: post-message (PostToolUse equivalent)\n * Priority: 70\n *\n * Estimates token usage from character counts (1 token \u2248 4 chars).\n * Accumulates in session state. Warns at 60%, 80%, 90% thresholds.\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\nimport { incrementSpending } from \"../spending/tracker.mjs\";\n\nexport interface HookInput {\n hook_type: \"PostToolUse\";\n tool_name?: string;\n tool_input?: unknown;\n tool_output?: unknown;\n session_id?: string;\n}\n\nexport interface HookOutput {\n modifiedResult?: unknown;\n status: \"ok\" | \"skip\" | \"error\";\n latencyMs: number;\n mutations: Array<{ type: \"set_token_budget\"; budget: number } | { type: \"emit_hud\"; hudEmit: unknown } | { type: \"log\"; level: \"info\" | \"warn\" | \"error\"; message: string }>;\n log: string[];\n}\n\ninterface SessionState {\n tokens_estimated: number;\n token_budget: number;\n context_pct: number;\n warnings_issued: Set<string>;\n}\n\n// Model context windows in tokens (for future model-specific budget lookup)\n// Exported for potential external use\nexport const MODEL_CONTEXTS = {\n \"claude-sonnet-4.5\": 200_000,\n \"claude-sonnet-4\": 200_000,\n \"claude-sonnet-4.6\": 200_000,\n \"claude-opus-4.6\": 200_000,\n \"gpt-5\": 128_000,\n \"gpt-5.4-mini\": 128_000,\n \"gemini-3-pro\": 128_000,\n default: 200_000,\n};\n\nconst WARNING_THRESHOLDS = [60, 80, 90];\n\nexport function estimateTokens(input: unknown): number {\n if (!input) return 0;\n try {\n const str = typeof input === \"string\" ? input : JSON.stringify(input);\n return Math.ceil(str.length / 4);\n } catch {\n return 0;\n }\n}\n\nfunction getStatePath(sessionId?: string): string {\n const base = join(homedir(), \".omp\", \"state\");\n if (sessionId) {\n return join(base, \"sessions\", sessionId, \"session.json\");\n }\n return join(base, \"session.json\");\n}\n\nfunction ensureDir(path: string): void {\n mkdirSync(path.substring(0, path.lastIndexOf(\"/\")), { recursive: true });\n}\n\nexport function processHook(input: HookInput): HookOutput {\n const start = Date.now();\n const log: string[] = [];\n\n try {\n if (input.hook_type !== \"PostToolUse\") {\n return {\n status: \"skip\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [],\n };\n }\n\n const statePath = getStatePath(input.session_id);\n let state: SessionState;\n\n try {\n const raw = JSON.parse(readFileSync(statePath, \"utf-8\")) as Record<string, unknown>;\n // warnings_issued is persisted as an array (a Set JSON-serializes to {}).\n // Rehydrate to a Set; tolerate legacy state files where it is {} or missing.\n state = {\n ...raw,\n warnings_issued: new Set(\n Array.isArray(raw.warnings_issued) ? (raw.warnings_issued as string[]) : []\n ),\n } as unknown as SessionState;\n } catch {\n // Initialize state if not found \u2014 budget derived from model when available\n const fallbackModel = (input as { model?: string }).model ?? \"default\";\n state = {\n tokens_estimated: 0,\n token_budget: (MODEL_CONTEXTS as Record<string, number>)[fallbackModel] ?? MODEL_CONTEXTS[\"default\"] ?? 200_000,\n context_pct: 0,\n warnings_issued: new Set(),\n };\n }\n\n const inputTokens = estimateTokens(input.tool_input);\n const outputTokens = estimateTokens(input.tool_output);\n const delta = inputTokens + outputTokens;\n\n state.tokens_estimated += delta;\n state.context_pct = Math.min(100, Math.round((state.tokens_estimated / state.token_budget) * 100));\n\n const mutations: HookOutput[\"mutations\"] = [\n { type: \"set_token_budget\", budget: state.token_budget },\n ];\n\n // Check warning thresholds\n for (const threshold of WARNING_THRESHOLDS) {\n const key = `warn_${threshold}`;\n if (state.context_pct >= threshold && !state.warnings_issued.has(key)) {\n state.warnings_issued.add(key);\n const message =\n threshold >= 90\n ? `CRITICAL: Context at ${state.context_pct}%. Tokens near budget limit.`\n : threshold >= 80\n ? `WARNING: Context at ${state.context_pct}%. Consider enabling ecomode.`\n : `INFO: Context at ${state.context_pct}%.`;\n mutations.push({ type: \"log\", level: threshold >= 80 ? \"warn\" : \"info\", message });\n log.push(message);\n }\n }\n\n // Write state back \u2014 persist warnings_issued as an array since a Set\n // JSON-serializes to {} and would break .has() on the next invocation.\n try {\n ensureDir(statePath);\n writeFileSync(\n statePath,\n JSON.stringify({ ...state, warnings_issued: Array.from(state.warnings_issued) }),\n \"utf-8\"\n );\n } catch (e) {\n log.push(`Failed to write state: ${e}`);\n }\n\n // Track premium request spending\n const sessionId = input.session_id ?? `omp-${Date.now()}`;\n try { incrementSpending(sessionId); } catch { /* non-blocking */ }\n\n return {\n status: \"ok\",\n latencyMs: Date.now() - start,\n mutations,\n log,\n };\n } catch (err) {\n return {\n status: \"error\",\n latencyMs: Date.now() - start,\n mutations: [],\n log: [`Error: ${err}`],\n };\n }\n}\n\n// Main entry point \u2014 only runs when executed directly (not imported).\n// Fail-open: any stdin/parse/processing failure still emits valid JSON and exits 0.\nimport { fileURLToPath } from \"url\";\nimport { runHookMain } from \"./runner.mts\";\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n await runHookMain(processHook, { hookName: \"token-tracker\" });\n}\n", "/**\n * Spending tracker for OMP.\n * Tracks premium API requests per session and per calendar month.\n * Persists to ~/.omp/state/spending-monthly.json\n *\n * // v1.1 known limitation: no /omp:spending reset command. To reset monthly counter manually: rm ~/.omp/state/spending-monthly.json\n */\n\nimport { readFileSync, writeFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join, dirname } from \"path\";\nimport type { SpendingState } from \"./types.mjs\";\n\nconst SPENDING_PATH = join(homedir(), \".omp\", \"state\", \"spending-monthly.json\");\n\nfunction currentMonth(): string {\n const now = new Date();\n return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n}\n\nexport function loadSpending(sessionId: string): SpendingState {\n let raw: SpendingState;\n try {\n raw = JSON.parse(readFileSync(SPENDING_PATH, \"utf-8\")) as SpendingState;\n } catch {\n // Missing or malformed file \u2014 start fresh\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month: currentMonth(),\n monthlyPremiumRequests: 0,\n };\n }\n\n const month = currentMonth();\n\n // Reset monthly counter when month rolls over\n if (raw.month !== month) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: 0,\n };\n }\n\n // Reset session counter when session changes\n if (raw.sessionId !== sessionId) {\n return {\n version: 1,\n sessionId,\n sessionPremiumRequests: 0,\n month,\n monthlyPremiumRequests: raw.monthlyPremiumRequests,\n };\n }\n\n return { ...raw, version: 1 };\n}\n\nexport function saveSpending(state: SpendingState): void {\n try {\n mkdirSync(dirname(SPENDING_PATH), { recursive: true });\n writeFileSync(SPENDING_PATH, JSON.stringify(state, null, 2), \"utf-8\");\n } catch (e) {\n console.warn(`[OMP] spending: failed to save state: ${e}`);\n }\n}\n\nexport function incrementSpending(sessionId: string): SpendingState {\n const state = loadSpending(sessionId);\n state.sessionPremiumRequests += 1;\n state.monthlyPremiumRequests += 1;\n saveSpending(state);\n return state;\n}\n", "/**\n * Shared hook entry-point runner.\n *\n * Hooks must be FAIL-OPEN: any failure (empty stdin, malformed JSON,\n * unexpected processing error) must still emit a valid HookOutput-shaped\n * JSON object on stdout and exit 0. A non-zero exit or non-JSON stdout\n * causes the Copilot CLI to treat the hook as errored, which denies the\n * tool call for PreToolUse hooks.\n *\n * Fail-open events are persisted (best-effort) as JSONL records to\n * ~/.omp/logs/hook-failures.jsonl and mirrored on stderr so failures\n * remain observable without ever touching the stdout JSON contract.\n */\n\nimport { appendFileSync, mkdirSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\nexport interface FailOpenOutput {\n decision?: \"allow\";\n status: \"error\";\n latencyMs: number;\n mutations: never[];\n log: string[];\n}\n\nexport interface RunHookOptions {\n /**\n * When true (hooks whose HookOutput supports a decision field), the\n * fail-open output includes `\"decision\": \"allow\"` so the tool call is\n * explicitly allowed.\n */\n failOpenDecision?: boolean;\n /** Hook id recorded in fail-open log entries (stderr + JSONL). */\n hookName?: string;\n}\n\nexport async function readStdin(): Promise<string> {\n const readStdinActual = async (): Promise<string> => {\n const chunks: string[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(String(chunk));\n }\n return chunks.join(\"\");\n };\n\n const stdinTimeout = new Promise<string>((resolve) =>\n setTimeout(\n () => resolve(\"\"),\n (parseInt(process.env.OMP_HOOK_STDIN_TIMEOUT_MS ?? \"500\") || 500)\n )\n );\n\n return Promise.race([readStdinActual(), stdinTimeout]);\n}\n\n/**\n * Best-effort persistence of a fail-open event. Wrapped in its own\n * try/catch: logging must NEVER break fail-open or the one-JSON-object\n * stdout contract. Writes to stderr and the JSONL log only \u2014 never stdout.\n */\nfunction logHookFailure(hook: string, reason: string): void {\n try {\n process.stderr.write(`[omp hook fail-open] ${hook}: ${reason}\\n`);\n } catch {\n // stderr unavailable \u2014 ignore\n }\n try {\n const logsDir = join(homedir(), \".omp\", \"logs\");\n mkdirSync(logsDir, { recursive: true });\n const record = JSON.stringify({ ts: new Date().toISOString(), hook, reason });\n appendFileSync(join(logsDir, \"hook-failures.jsonl\"), record + \"\\n\", \"utf-8\");\n } catch {\n // best-effort only \u2014 never let logging break fail-open\n }\n}\n\n/**\n * Reads HookInput JSON from stdin, runs the hook, and prints the\n * HookOutput JSON to stdout. Never throws, never exits non-zero,\n * never emits non-JSON to stdout.\n */\nexport async function runHookMain<TInput>(\n processHook: (input: TInput) => unknown,\n options: RunHookOptions = {}\n): Promise<void> {\n let outputJson: string;\n try {\n const input = JSON.parse(await readStdin()) as TInput;\n const serialized = JSON.stringify(processHook(input));\n if (typeof serialized !== \"string\") {\n throw new Error(\"hook produced no serializable output\");\n }\n outputJson = serialized;\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logHookFailure(options.hookName ?? \"unknown\", reason);\n const failOpen: FailOpenOutput = {\n ...(options.failOpenDecision ? { decision: \"allow\" as const } : {}),\n status: \"error\",\n latencyMs: 0,\n mutations: [],\n log: [`fail-open: ${reason}`],\n };\n outputJson = JSON.stringify(failOpen);\n }\n console.log(outputJson);\n process.exitCode = 0;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["readFileSync", "writeFileSync", "mkdirSync", "homedir", "join", "mkdirSync", "homedir", "join", "processHook", "join", "homedir", "mkdirSync", "readFileSync", "writeFileSync"]
|
|
7
7
|
}
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -28324,7 +28324,7 @@ var TOOLS = [
|
|
|
28324
28324
|
},
|
|
28325
28325
|
{
|
|
28326
28326
|
name: "omp_get_agents",
|
|
28327
|
-
description: "List all
|
|
28327
|
+
description: "List all 19 OMP agents with their IDs, tiers, tools, and roles",
|
|
28328
28328
|
inputSchema: { type: "object", properties: {} }
|
|
28329
28329
|
},
|
|
28330
28330
|
{
|
|
@@ -28336,24 +28336,25 @@ var TOOLS = [
|
|
|
28336
28336
|
agentId: {
|
|
28337
28337
|
type: "string",
|
|
28338
28338
|
enum: [
|
|
28339
|
-
"
|
|
28340
|
-
"explorer",
|
|
28341
|
-
"planner",
|
|
28342
|
-
"executor",
|
|
28343
|
-
"verifier",
|
|
28344
|
-
"writer",
|
|
28345
|
-
"reviewer",
|
|
28346
|
-
"designer",
|
|
28347
|
-
"researcher",
|
|
28348
|
-
"tester",
|
|
28349
|
-
"debugger",
|
|
28339
|
+
"analyst",
|
|
28350
28340
|
"architect",
|
|
28341
|
+
"code-reviewer",
|
|
28342
|
+
"code-simplifier",
|
|
28343
|
+
"critic",
|
|
28344
|
+
"debugger",
|
|
28345
|
+
"designer",
|
|
28346
|
+
"document-specialist",
|
|
28347
|
+
"executor",
|
|
28348
|
+
"explore",
|
|
28351
28349
|
"git-master",
|
|
28350
|
+
"planner",
|
|
28351
|
+
"qa-tester",
|
|
28352
|
+
"scientist",
|
|
28352
28353
|
"security-reviewer",
|
|
28353
|
-
"
|
|
28354
|
-
"
|
|
28355
|
-
"
|
|
28356
|
-
"
|
|
28354
|
+
"test-engineer",
|
|
28355
|
+
"tracer",
|
|
28356
|
+
"verifier",
|
|
28357
|
+
"writer"
|
|
28357
28358
|
]
|
|
28358
28359
|
},
|
|
28359
28360
|
task: { type: "string", description: "Task description" },
|