@kisev/agentomatic 11.0.0-dev.46.gfbe1e4c6992e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/README.ru.md +98 -0
- package/dist/agent-profiles.d.ts +125 -0
- package/dist/agent-profiles.js +820 -0
- package/dist/assets/agents/architect.md +67 -0
- package/dist/assets/agents/critic.md +55 -0
- package/dist/assets/agents/manager.md +40 -0
- package/dist/assets/agents/mapper.md +33 -0
- package/dist/assets/agents/review.md +37 -0
- package/dist/assets/agents/worker.md +78 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/briefing.md +10 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/mattermost-triage.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk-stats.md +13 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +13 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/taskmatic.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +301 -0
- package/dist/assets/plugins/memomatic.js +3 -0
- package/dist/assets/plugins/rtk.js +3 -0
- package/dist/assets/plugins/rules-injector.js +3 -0
- package/dist/assets/plugins/zed-bell.js +3 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +39 -0
- package/dist/cli-output.d.ts +27 -0
- package/dist/cli-output.js +296 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1135 -0
- package/dist/config-setup.d.ts +71 -0
- package/dist/config-setup.js +438 -0
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +102 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +638 -0
- package/dist/generate-assets.d.ts +1 -0
- package/dist/generate-assets.js +54 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +141 -0
- package/dist/installer.d.ts +56 -0
- package/dist/installer.js +710 -0
- package/dist/jsonc.d.ts +56 -0
- package/dist/jsonc.js +432 -0
- package/dist/lifecycle.d.ts +77 -0
- package/dist/lifecycle.js +736 -0
- package/dist/package-metadata.d.ts +4 -0
- package/dist/package-metadata.js +42 -0
- package/dist/plugins/memomatic.d.ts +76 -0
- package/dist/plugins/memomatic.js +101 -0
- package/dist/plugins/rtk.d.ts +41 -0
- package/dist/plugins/rtk.js +177 -0
- package/dist/plugins/rules-injector.d.ts +22 -0
- package/dist/plugins/rules-injector.js +131 -0
- package/dist/plugins/zed-bell.d.ts +13 -0
- package/dist/plugins/zed-bell.js +14 -0
- package/dist/reconcile.d.ts +47 -0
- package/dist/reconcile.js +490 -0
- package/dist/registry.d.ts +21 -0
- package/dist/registry.js +92 -0
- package/dist/routing.d.ts +133 -0
- package/dist/routing.js +578 -0
- package/dist/runtime/state.d.ts +9 -0
- package/dist/runtime/state.js +193 -0
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/dist/terminal-wizard.d.ts +3 -0
- package/dist/terminal-wizard.js +237 -0
- package/package.json +72 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const packageJson = resolve(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
5
|
+
let cachedMetadata;
|
|
6
|
+
function readPackageMetadata() {
|
|
7
|
+
if (cachedMetadata !== undefined)
|
|
8
|
+
return cachedMetadata;
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
11
|
+
cachedMetadata =
|
|
12
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: null;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
cachedMetadata = null;
|
|
18
|
+
}
|
|
19
|
+
return cachedMetadata;
|
|
20
|
+
}
|
|
21
|
+
function readString(name) {
|
|
22
|
+
const value = readPackageMetadata()?.[name];
|
|
23
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
24
|
+
}
|
|
25
|
+
export function readPackageVersion() {
|
|
26
|
+
return readString("version");
|
|
27
|
+
}
|
|
28
|
+
export function requirePackageVersion() {
|
|
29
|
+
const version = readPackageVersion();
|
|
30
|
+
if (!version)
|
|
31
|
+
throw new Error("Package version is unavailable");
|
|
32
|
+
return version;
|
|
33
|
+
}
|
|
34
|
+
export function requireSkillsInstallerVersion() {
|
|
35
|
+
const version = readString("skillsInstallerVersion");
|
|
36
|
+
if (!version)
|
|
37
|
+
throw new Error("Skills installer version is unavailable");
|
|
38
|
+
return version;
|
|
39
|
+
}
|
|
40
|
+
export function skillsInstallerSpec() {
|
|
41
|
+
return `skills@${requireSkillsInstallerVersion()}`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type MemomaticOptions = {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
bootstrapBudgetChars?: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function memomatic(options?: MemomaticOptions): Promise<{
|
|
6
|
+
"experimental.chat.system.transform"?: undefined;
|
|
7
|
+
tool?: undefined;
|
|
8
|
+
} | {
|
|
9
|
+
"experimental.chat.system.transform": (_input: unknown, output: {
|
|
10
|
+
system: string[];
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
tool: {
|
|
13
|
+
memory_forget: {
|
|
14
|
+
description: string;
|
|
15
|
+
args: {
|
|
16
|
+
file: import("zod").ZodString;
|
|
17
|
+
line: import("zod").ZodNumber;
|
|
18
|
+
};
|
|
19
|
+
execute(args: {
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
23
|
+
};
|
|
24
|
+
memory_get: {
|
|
25
|
+
description: string;
|
|
26
|
+
args: {
|
|
27
|
+
file: import("zod").ZodString;
|
|
28
|
+
line: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
29
|
+
};
|
|
30
|
+
execute(args: {
|
|
31
|
+
file: string;
|
|
32
|
+
line?: number | undefined;
|
|
33
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
34
|
+
};
|
|
35
|
+
memory_search: {
|
|
36
|
+
description: string;
|
|
37
|
+
args: {
|
|
38
|
+
query: import("zod").ZodString;
|
|
39
|
+
};
|
|
40
|
+
execute(args: {
|
|
41
|
+
query: string;
|
|
42
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
43
|
+
};
|
|
44
|
+
memory_write: {
|
|
45
|
+
description: string;
|
|
46
|
+
args: {
|
|
47
|
+
text: import("zod").ZodString;
|
|
48
|
+
target: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
49
|
+
curated: "curated";
|
|
50
|
+
episodic: "episodic";
|
|
51
|
+
user: "user";
|
|
52
|
+
}>>;
|
|
53
|
+
origin: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
54
|
+
agent: "agent";
|
|
55
|
+
user: "user";
|
|
56
|
+
}>>;
|
|
57
|
+
key: import("zod").ZodOptional<import("zod").ZodString>;
|
|
58
|
+
project: import("zod").ZodOptional<import("zod").ZodString>;
|
|
59
|
+
importance: import("zod").ZodOptional<import("zod").ZodNumber>;
|
|
60
|
+
trigger: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
|
|
61
|
+
pinned: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
62
|
+
};
|
|
63
|
+
execute(args: {
|
|
64
|
+
text: string;
|
|
65
|
+
target?: "curated" | "episodic" | "user" | undefined;
|
|
66
|
+
origin?: "agent" | "user" | undefined;
|
|
67
|
+
key?: string | undefined;
|
|
68
|
+
project?: string | undefined;
|
|
69
|
+
importance?: number | undefined;
|
|
70
|
+
trigger?: string[] | undefined;
|
|
71
|
+
pinned?: boolean | undefined;
|
|
72
|
+
}, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}>;
|
|
76
|
+
export default memomatic;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry, } from "@kisev/memomatic";
|
|
3
|
+
const DEFAULT_BOOTSTRAP_BUDGET = 4_000;
|
|
4
|
+
async function withContext(work) {
|
|
5
|
+
const context = await openMemomatic();
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(await work(context));
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
context.store.close();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function memomatic(options = {}) {
|
|
14
|
+
if (options.enabled === false)
|
|
15
|
+
return {};
|
|
16
|
+
const budget = options.bootstrapBudgetChars ?? DEFAULT_BOOTSTRAP_BUDGET;
|
|
17
|
+
const memorySearch = tool({
|
|
18
|
+
args: { query: tool.schema.string() },
|
|
19
|
+
description: "Search personal long-term memory for durable decisions, discoveries, failed attempts, and session outcomes.",
|
|
20
|
+
async execute(args) {
|
|
21
|
+
return withContext((context) => searchMemory(context, args.query));
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const memoryGet = tool({
|
|
25
|
+
args: {
|
|
26
|
+
file: tool.schema.string(),
|
|
27
|
+
line: tool.schema.number().optional(),
|
|
28
|
+
},
|
|
29
|
+
description: "Read a memomatic memory file fully or one line. Fetching a line marks that entry as useful.",
|
|
30
|
+
async execute(args) {
|
|
31
|
+
return withContext((context) => getEntry(context, args.file, args.line));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const memoryWrite = tool({
|
|
35
|
+
args: {
|
|
36
|
+
text: tool.schema.string(),
|
|
37
|
+
target: tool.schema.enum(["episodic", "curated", "user"]).optional(),
|
|
38
|
+
origin: tool.schema.enum(["user", "agent"]).optional(),
|
|
39
|
+
key: tool.schema.string().optional(),
|
|
40
|
+
project: tool.schema.string().optional(),
|
|
41
|
+
importance: tool.schema.number().min(1).max(10).optional(),
|
|
42
|
+
trigger: tool.schema.array(tool.schema.string()).optional(),
|
|
43
|
+
pinned: tool.schema.boolean().optional(),
|
|
44
|
+
},
|
|
45
|
+
description: "Save a durable memory entry: standing decisions with rationale, discoveries, failed attempts with rejection reasons, session outcomes, action-sensitive boundaries. Respect MEMORY_RULES.md; never-save topics are rejected.",
|
|
46
|
+
async execute(args) {
|
|
47
|
+
return withContext((context) => writeEntry(context, {
|
|
48
|
+
importance: args.importance,
|
|
49
|
+
key: args.key,
|
|
50
|
+
origin: args.origin,
|
|
51
|
+
pinned: args.pinned,
|
|
52
|
+
project: args.project,
|
|
53
|
+
target: args.target,
|
|
54
|
+
text: args.text,
|
|
55
|
+
trigger: args.trigger,
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const memoryForget = tool({
|
|
60
|
+
args: { file: tool.schema.string(), line: tool.schema.number() },
|
|
61
|
+
description: "Remove one entry line from a memomatic memory file (explicit deletion only).",
|
|
62
|
+
async execute(args) {
|
|
63
|
+
return withContext((context) => forgetEntry(context, args));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
68
|
+
try {
|
|
69
|
+
const context = await openMemomatic();
|
|
70
|
+
try {
|
|
71
|
+
const blocks = [];
|
|
72
|
+
for (const file of [context.paths.memoryFile, context.paths.userFile]) {
|
|
73
|
+
const content = await import("node:fs/promises")
|
|
74
|
+
.then((fs) => fs.readFile(file, "utf8"))
|
|
75
|
+
.catch(() => undefined);
|
|
76
|
+
if (content === undefined || !content.trim())
|
|
77
|
+
continue;
|
|
78
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
79
|
+
blocks.push(`# Memomatic ${name}\n\n${content.trim().slice(0, budget)}`);
|
|
80
|
+
}
|
|
81
|
+
if (!blocks.length)
|
|
82
|
+
return;
|
|
83
|
+
output.system.push(`${blocks.join("\n\n")}\n\nUse memory_search for episodic details and memory_write to save durable outcomes. Never re-save content that is already present in this memory.`);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
context.store.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Memory bootstrap must never break the session.
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
tool: {
|
|
94
|
+
memory_forget: memoryForget,
|
|
95
|
+
memory_get: memoryGet,
|
|
96
|
+
memory_search: memorySearch,
|
|
97
|
+
memory_write: memoryWrite,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export default memomatic;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type HookInput = {
|
|
2
|
+
tool: string;
|
|
3
|
+
args?: {
|
|
4
|
+
command?: unknown;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
type HookOutput = {
|
|
8
|
+
output?: unknown;
|
|
9
|
+
};
|
|
10
|
+
export type RtkMethod = "compressed-rtk" | "truncated-head-tail" | "rtk-unavailable" | "ineligible" | "below-threshold";
|
|
11
|
+
export type RtkStats = {
|
|
12
|
+
schema_version: 1;
|
|
13
|
+
counters: Record<RtkMethod, number>;
|
|
14
|
+
chars_original: number;
|
|
15
|
+
chars_final: number;
|
|
16
|
+
recent: Array<{
|
|
17
|
+
at: string;
|
|
18
|
+
method: RtkMethod;
|
|
19
|
+
original: number;
|
|
20
|
+
final: number;
|
|
21
|
+
}>;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
};
|
|
24
|
+
export type RtkOptions = {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
bin?: string;
|
|
27
|
+
threshold?: number;
|
|
28
|
+
run?: (filter: string, input: string) => Promise<string | undefined>;
|
|
29
|
+
statsPath?: string | null;
|
|
30
|
+
};
|
|
31
|
+
export declare function emptyRtkStats(): RtkStats;
|
|
32
|
+
export declare function rtkStatsPath(home?: string): string;
|
|
33
|
+
export declare function rtkCharsSaved(stats: RtkStats): number;
|
|
34
|
+
export declare function rtkTokensSavedEstimate(stats: RtkStats): number;
|
|
35
|
+
export declare function readRtkStats(path?: string): Promise<RtkStats | undefined>;
|
|
36
|
+
export declare function rtk(options?: RtkOptions): Promise<{
|
|
37
|
+
"tool.execute.after"?: undefined;
|
|
38
|
+
} | {
|
|
39
|
+
"tool.execute.after": (input: HookInput, output: HookOutput) => Promise<void>;
|
|
40
|
+
}>;
|
|
41
|
+
export default rtk;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { stateRoot, withStateLock } from "../runtime/state.js";
|
|
6
|
+
import { writeAtomic } from "../lifecycle.js";
|
|
7
|
+
const THRESHOLD = 8_000;
|
|
8
|
+
const FILTERS = [
|
|
9
|
+
{ prefix: ["git", "log"], filter: "git-log" },
|
|
10
|
+
{ prefix: ["git", "diff"], filter: "git-diff" },
|
|
11
|
+
{ prefix: ["git", "status"], filter: "git-status" },
|
|
12
|
+
{ prefix: ["rg"], filter: "grep" },
|
|
13
|
+
{ prefix: ["grep"], filter: "grep" },
|
|
14
|
+
{ prefix: ["pytest"], filter: "pytest" },
|
|
15
|
+
{ prefix: ["tsc"], filter: "tsc" },
|
|
16
|
+
];
|
|
17
|
+
const RECENT_EVENTS_CAP = 20;
|
|
18
|
+
const CHARS_PER_TOKEN = 4;
|
|
19
|
+
export function emptyRtkStats() {
|
|
20
|
+
return {
|
|
21
|
+
schema_version: 1,
|
|
22
|
+
counters: {
|
|
23
|
+
"compressed-rtk": 0,
|
|
24
|
+
"truncated-head-tail": 0,
|
|
25
|
+
"rtk-unavailable": 0,
|
|
26
|
+
ineligible: 0,
|
|
27
|
+
"below-threshold": 0,
|
|
28
|
+
},
|
|
29
|
+
chars_original: 0,
|
|
30
|
+
chars_final: 0,
|
|
31
|
+
recent: [],
|
|
32
|
+
updated_at: "",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function rtkStatsPath(home = homedir()) {
|
|
36
|
+
return join(stateRoot("rtk", home), "stats.json");
|
|
37
|
+
}
|
|
38
|
+
export function rtkCharsSaved(stats) {
|
|
39
|
+
return Math.max(0, stats.chars_original - stats.chars_final);
|
|
40
|
+
}
|
|
41
|
+
export function rtkTokensSavedEstimate(stats) {
|
|
42
|
+
return Math.ceil(rtkCharsSaved(stats) / CHARS_PER_TOKEN);
|
|
43
|
+
}
|
|
44
|
+
export async function readRtkStats(path = rtkStatsPath()) {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = await readFile(path, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const value = JSON.parse(raw);
|
|
54
|
+
if (value.schema_version !== 1 ||
|
|
55
|
+
!value.counters ||
|
|
56
|
+
typeof value.counters !== "object" ||
|
|
57
|
+
Array.isArray(value.counters))
|
|
58
|
+
return undefined;
|
|
59
|
+
const base = emptyRtkStats();
|
|
60
|
+
for (const method of Object.keys(base.counters)) {
|
|
61
|
+
const counter = value.counters[method];
|
|
62
|
+
if (Number.isSafeInteger(counter) && counter >= 0)
|
|
63
|
+
base.counters[method] = counter;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...base,
|
|
67
|
+
chars_original: Number.isSafeInteger(value.chars_original) ? value.chars_original : 0,
|
|
68
|
+
chars_final: Number.isSafeInteger(value.chars_final) ? value.chars_final : 0,
|
|
69
|
+
recent: Array.isArray(value.recent)
|
|
70
|
+
? value.recent.filter((item) => Boolean(item) &&
|
|
71
|
+
typeof item === "object" &&
|
|
72
|
+
typeof item.at === "string" &&
|
|
73
|
+
typeof item.method === "string" &&
|
|
74
|
+
item.method in base.counters &&
|
|
75
|
+
typeof item.original === "number" &&
|
|
76
|
+
typeof item.final === "number")
|
|
77
|
+
: [],
|
|
78
|
+
updated_at: typeof value.updated_at === "string" ? value.updated_at : "",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function recordStats(path, method, original, final) {
|
|
86
|
+
const boundary = dirname(path);
|
|
87
|
+
await withStateLock(boundary, async () => {
|
|
88
|
+
const current = (await readRtkStats(path)) ?? emptyRtkStats();
|
|
89
|
+
current.counters[method] += 1;
|
|
90
|
+
if (method !== "below-threshold") {
|
|
91
|
+
current.chars_original += original;
|
|
92
|
+
current.chars_final += final;
|
|
93
|
+
}
|
|
94
|
+
current.recent = [
|
|
95
|
+
...current.recent.filter((item) => item.at !== ""),
|
|
96
|
+
{ at: new Date().toISOString(), method, original, final },
|
|
97
|
+
].slice(-RECENT_EVENTS_CAP);
|
|
98
|
+
current.updated_at = new Date().toISOString();
|
|
99
|
+
await writeAtomic(path, Buffer.from(`${JSON.stringify(current)}\n`), 0o600);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function filter(command) {
|
|
103
|
+
if (/[|;$&\n()`]/.test(command))
|
|
104
|
+
return undefined;
|
|
105
|
+
if (/\b(--json|--format(?:=|\s)|diagnos|security|deploy|migrat|delete|remove)\b/i.test(command))
|
|
106
|
+
return undefined;
|
|
107
|
+
const values = command.trim().split(/\s+/);
|
|
108
|
+
return FILTERS.find((item) => item.prefix.every((part, index) => values[index] === part))?.filter;
|
|
109
|
+
}
|
|
110
|
+
function truncate(value) {
|
|
111
|
+
const points = Array.from(value);
|
|
112
|
+
const size = 3_200;
|
|
113
|
+
return points.length <= size * 2
|
|
114
|
+
? value
|
|
115
|
+
: `${points.slice(0, size).join("")}\n…\n${points.slice(-size).join("")}`;
|
|
116
|
+
}
|
|
117
|
+
function external(binary, selected, input) {
|
|
118
|
+
return new Promise((done) => {
|
|
119
|
+
const child = spawn(binary, ["pipe", "--filter", selected], {
|
|
120
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
121
|
+
timeout: 5_000,
|
|
122
|
+
});
|
|
123
|
+
let output = "";
|
|
124
|
+
child.stdout.setEncoding("utf8");
|
|
125
|
+
child.stdout.on("data", (chunk) => {
|
|
126
|
+
output += chunk;
|
|
127
|
+
});
|
|
128
|
+
child.once("error", () => done(undefined));
|
|
129
|
+
child.once("close", (code) => done(code === 0 ? output : undefined));
|
|
130
|
+
child.stdin.end(input, "utf8");
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export async function rtk(options = {}) {
|
|
134
|
+
if (options.enabled === false)
|
|
135
|
+
return {};
|
|
136
|
+
const run = options.run ??
|
|
137
|
+
((selected, input) => external(options.bin ?? "rtk", selected, input));
|
|
138
|
+
const statsPath = options.statsPath === null ? undefined : (options.statsPath ?? rtkStatsPath());
|
|
139
|
+
return {
|
|
140
|
+
"tool.execute.after": async (input, output) => {
|
|
141
|
+
try {
|
|
142
|
+
if (input.tool === "edit" &&
|
|
143
|
+
typeof output.output === "string" &&
|
|
144
|
+
/oldString (not found|found multiple times|and newString must be different)/i.test(output.output))
|
|
145
|
+
output.output = `${output.output}\nSTOP. Read the file before retrying Edit.`;
|
|
146
|
+
if (input.tool !== "bash" ||
|
|
147
|
+
typeof output.output !== "string" ||
|
|
148
|
+
output.output.length < (options.threshold ?? THRESHOLD)) {
|
|
149
|
+
if (input.tool === "bash" && typeof output.output === "string" && statsPath)
|
|
150
|
+
await recordStats(statsPath, "below-threshold", output.output.length, output.output.length);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
|
|
154
|
+
const compressed = selected ? await run(selected, output.output) : undefined;
|
|
155
|
+
const result = compressed && compressed.length < output.output.length
|
|
156
|
+
? compressed
|
|
157
|
+
: truncate(output.output);
|
|
158
|
+
const originalSize = output.output.length;
|
|
159
|
+
output.output = `${result}\n[rtk: compressed method=${result === compressed ? `rtk/${selected}` : "head+tail"}; sizes=${originalSize}->${result.length}; evidence_complete=false; loss=possible]`;
|
|
160
|
+
if (statsPath) {
|
|
161
|
+
const method = result === compressed
|
|
162
|
+
? "compressed-rtk"
|
|
163
|
+
: selected === undefined
|
|
164
|
+
? "ineligible"
|
|
165
|
+
: compressed === undefined
|
|
166
|
+
? "rtk-unavailable"
|
|
167
|
+
: "truncated-head-tail";
|
|
168
|
+
await recordStats(statsPath, method, originalSize, result.length);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
/* RTK is fail-open: preserve original tool output on plugin failure. */
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export default rtk;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type RulesInjectorOptions = {
|
|
2
|
+
budget?: number;
|
|
3
|
+
cwd?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function rulesInjector(options?: RulesInjectorOptions): Promise<{
|
|
7
|
+
"tool.execute.after"?: undefined;
|
|
8
|
+
event?: undefined;
|
|
9
|
+
"experimental.chat.system.transform"?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
"tool.execute.after": (input: unknown, result: unknown) => Promise<void>;
|
|
12
|
+
event: ({ event, }: {
|
|
13
|
+
event?: {
|
|
14
|
+
type?: string;
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
"experimental.chat.system.transform": (input: unknown, result: {
|
|
19
|
+
system?: unknown[];
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
}>;
|
|
22
|
+
export default rulesInjector;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
const NAME = "AGENTS.md";
|
|
5
|
+
function sessionID(value) {
|
|
6
|
+
const item = value;
|
|
7
|
+
for (const key of ["sessionID", "sessionId", "session_id"])
|
|
8
|
+
if (typeof item?.[key] === "string" && item[key])
|
|
9
|
+
return item[key];
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function pathOf(input, output) {
|
|
13
|
+
for (const value of [input, output]) {
|
|
14
|
+
const args = value?.args;
|
|
15
|
+
for (const key of ["filePath", "file_path", "path"])
|
|
16
|
+
if (typeof args?.[key] === "string")
|
|
17
|
+
return args[key];
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function output(value, text) {
|
|
22
|
+
const item = value;
|
|
23
|
+
if (typeof item?.output === "string")
|
|
24
|
+
item.output = `${item.output}\n${text}`;
|
|
25
|
+
}
|
|
26
|
+
function marker(rule) {
|
|
27
|
+
return `[rules-injector source=${rule.path} revision=${rule.revision}]`;
|
|
28
|
+
}
|
|
29
|
+
function block(rule) {
|
|
30
|
+
return `${marker(rule)}\n${rule.content}\n[/rules-injector source=${rule.path}]`;
|
|
31
|
+
}
|
|
32
|
+
export async function rulesInjector(options = {}) {
|
|
33
|
+
if (options.enabled === false)
|
|
34
|
+
return {};
|
|
35
|
+
const budget = options.budget ?? 12_000;
|
|
36
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
37
|
+
const global = resolve(process.env.XDG_CONFIG_HOME ?? resolve(homedir(), ".config"), "opencode");
|
|
38
|
+
const sessions = new Map();
|
|
39
|
+
const state = (identifier) => {
|
|
40
|
+
let item = sessions.get(identifier);
|
|
41
|
+
if (!item) {
|
|
42
|
+
item = { loaded: new Map(), injected: new Set(), used: 0, replay: false };
|
|
43
|
+
sessions.set(identifier, item);
|
|
44
|
+
}
|
|
45
|
+
return item;
|
|
46
|
+
};
|
|
47
|
+
const inject = async (input, result) => {
|
|
48
|
+
const call = input;
|
|
49
|
+
if (call?.tool !== "read" && call?.tool !== "edit")
|
|
50
|
+
return;
|
|
51
|
+
const identifier = sessionID(input);
|
|
52
|
+
const target = pathOf(input, result);
|
|
53
|
+
if (!identifier || !target)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
const native = typeof call?.directory === "string" ? call.directory : cwd;
|
|
57
|
+
const paths = [];
|
|
58
|
+
let current = dirname(resolve(native, target));
|
|
59
|
+
while (true) {
|
|
60
|
+
const candidate = resolve(current, NAME);
|
|
61
|
+
try {
|
|
62
|
+
const info = await lstat(candidate);
|
|
63
|
+
if (info.isFile() &&
|
|
64
|
+
candidate !== resolve(native, NAME) &&
|
|
65
|
+
candidate !== resolve(global, NAME))
|
|
66
|
+
paths.push(await realpath(candidate));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ENOENT")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const parent = dirname(current);
|
|
73
|
+
if (parent === current)
|
|
74
|
+
break;
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
const currentState = state(identifier);
|
|
78
|
+
const additions = [];
|
|
79
|
+
for (const path of paths.reverse()) {
|
|
80
|
+
if (currentState.injected.has(path))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const info = await lstat(path);
|
|
84
|
+
if (!info.isFile())
|
|
85
|
+
continue;
|
|
86
|
+
const rule = {
|
|
87
|
+
path,
|
|
88
|
+
revision: `${Math.trunc(info.mtimeMs)}:${info.size}`,
|
|
89
|
+
content: await readFile(path, "utf8"),
|
|
90
|
+
};
|
|
91
|
+
const text = block(rule);
|
|
92
|
+
if (currentState.used + text.length > budget) {
|
|
93
|
+
additions.push(`[rules-injector warning] context budget exhausted; skipped ${path}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
currentState.loaded.set(path, rule);
|
|
97
|
+
currentState.injected.add(path);
|
|
98
|
+
currentState.used += text.length;
|
|
99
|
+
additions.push(text);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
additions.push(`[rules-injector warning] cannot read ${path}: ${String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (additions.length)
|
|
106
|
+
output(result, additions.join("\n\n"));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
output(result, `[rules-injector warning] cannot read AGENTS.md: ${String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
"tool.execute.after": inject,
|
|
114
|
+
event: async ({ event, }) => {
|
|
115
|
+
if (event?.type?.includes("compaction")) {
|
|
116
|
+
const identifier = sessionID(event.properties);
|
|
117
|
+
if (identifier)
|
|
118
|
+
state(identifier).replay = true;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"experimental.chat.system.transform": async (input, result) => {
|
|
122
|
+
const identifier = sessionID(input);
|
|
123
|
+
if (!identifier || !state(identifier).replay || !Array.isArray(result.system))
|
|
124
|
+
return;
|
|
125
|
+
for (const rule of state(identifier).loaded.values())
|
|
126
|
+
result.system.push(block(rule));
|
|
127
|
+
state(identifier).replay = false;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export default rulesInjector;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ZedBellOptions = {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
};
|
|
4
|
+
export declare function zedBell(options?: ZedBellOptions): Promise<{
|
|
5
|
+
event?: undefined;
|
|
6
|
+
} | {
|
|
7
|
+
event: ({ event }: {
|
|
8
|
+
event: {
|
|
9
|
+
type?: string;
|
|
10
|
+
};
|
|
11
|
+
}) => Promise<void>;
|
|
12
|
+
}>;
|
|
13
|
+
export default zedBell;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function zedBell(options = {}) {
|
|
2
|
+
if (!options.enabled)
|
|
3
|
+
return {};
|
|
4
|
+
return {
|
|
5
|
+
event: async ({ event }) => {
|
|
6
|
+
if (event.type !== "session.idle" && event.type !== "permission.asked")
|
|
7
|
+
return;
|
|
8
|
+
const acp = process.env.OPENCODE_CLIENT === "acp" || process.argv.includes("acp");
|
|
9
|
+
if (!acp)
|
|
10
|
+
process.stdout.write("\x07");
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export default zedBell;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LifecycleError, type SupersededPlan, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
|
+
export type ReconcileStatus = "current" | "retired" | "renamed" | "modified-managed" | "user-owned" | "unknown" | "conflict" | "diagnostic-state-only";
|
|
3
|
+
export type ReconcileItem = {
|
|
4
|
+
path: string;
|
|
5
|
+
status: ReconcileStatus;
|
|
6
|
+
reason: string;
|
|
7
|
+
sha256?: string;
|
|
8
|
+
replacement?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type ReconcilePlan = {
|
|
11
|
+
schema_version: 2;
|
|
12
|
+
domain: "reconcile";
|
|
13
|
+
scope: Scope;
|
|
14
|
+
root: string;
|
|
15
|
+
inventory_version: string;
|
|
16
|
+
current: ReconcileItem[];
|
|
17
|
+
retired: ReconcileItem[];
|
|
18
|
+
renamed: ReconcileItem[];
|
|
19
|
+
modified_managed: ReconcileItem[];
|
|
20
|
+
user_owned: ReconcileItem[];
|
|
21
|
+
unknown: ReconcileItem[];
|
|
22
|
+
conflicts: ReconcileItem[];
|
|
23
|
+
diagnostic_state_only: ReconcileItem[];
|
|
24
|
+
operations: Array<{
|
|
25
|
+
path: string;
|
|
26
|
+
operation: "archive" | "remove" | "write";
|
|
27
|
+
sha256?: string;
|
|
28
|
+
}>;
|
|
29
|
+
plan_digest: string;
|
|
30
|
+
confirmation_digest?: string;
|
|
31
|
+
superseded_plan?: SupersededPlan;
|
|
32
|
+
confirmable: boolean;
|
|
33
|
+
digest?: string;
|
|
34
|
+
receipt_expires_at?: string;
|
|
35
|
+
};
|
|
36
|
+
export type ReconcileResult = {
|
|
37
|
+
status: "ok";
|
|
38
|
+
applied: true;
|
|
39
|
+
plan: ReconcilePlan;
|
|
40
|
+
};
|
|
41
|
+
export type ReconcileOptions = TransactionOptions;
|
|
42
|
+
export declare class ReconcileError extends LifecycleError {
|
|
43
|
+
}
|
|
44
|
+
export declare function previewReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
|
|
45
|
+
/** Build the ownership classification without receipts, locks, or recovery. */
|
|
46
|
+
export declare function inspectReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
|
|
47
|
+
export declare function applyReconcile(scope: Scope, confirmationDigest: string, cwd?: string, home?: string, options?: ReconcileOptions): Promise<ReconcileResult>;
|