cortex-agents 2.2.0 → 2.3.1

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import * as plan from "./tools/plan";
6
6
  import * as session from "./tools/session";
7
7
  import * as docs from "./tools/docs";
8
8
  import * as task from "./tools/task";
9
- // Agent descriptions for handover toast notifications
9
+ // ─── Agent Descriptions (for handover toasts) ───────────────────────────────
10
10
  const AGENT_DESCRIPTIONS = {
11
11
  build: "Development mode — ready to implement",
12
12
  plan: "Planning mode — read-only analysis",
@@ -16,21 +16,110 @@ const AGENT_DESCRIPTIONS = {
16
16
  security: "Security subagent — vulnerability audit",
17
17
  devops: "DevOps subagent — CI/CD and deployment",
18
18
  };
19
+ const TOOL_NOTIFICATIONS = {
20
+ task_finalize: {
21
+ successTitle: "Task Finalized",
22
+ successMsg: (args) => `Committed & pushed: ${(args.commitMessage ?? "").substring(0, 50)}`,
23
+ errorTitle: "Finalization Failed",
24
+ errorMsg: (_, out) => out
25
+ .replace(/^✗\s*/, "")
26
+ .split("\n")[0]
27
+ .substring(0, 100),
28
+ successDuration: 5000,
29
+ errorDuration: 10000,
30
+ },
31
+ plan_save: {
32
+ successTitle: "Plan Saved",
33
+ successMsg: (args) => args.title ?? "Plan saved",
34
+ errorTitle: "Plan Save Failed",
35
+ errorMsg: (_, out) => out.substring(0, 100),
36
+ },
37
+ plan_delete: {
38
+ successTitle: "Plan Deleted",
39
+ successMsg: (args) => args.filename ?? "Plan deleted",
40
+ errorTitle: "Plan Delete Failed",
41
+ errorMsg: (_, out) => out.substring(0, 100),
42
+ },
43
+ session_save: {
44
+ successTitle: "Session Saved",
45
+ successMsg: () => "Session summary recorded",
46
+ errorTitle: "Session Save Failed",
47
+ errorMsg: (_, out) => out.substring(0, 100),
48
+ },
49
+ docs_save: {
50
+ successTitle: "Documentation Saved",
51
+ successMsg: (args) => `${args.type ?? "doc"}: ${args.title ?? "Untitled"}`,
52
+ errorTitle: "Doc Save Failed",
53
+ errorMsg: (_, out) => out.substring(0, 100),
54
+ },
55
+ docs_init: {
56
+ successTitle: "Docs Initialized",
57
+ successMsg: () => "Documentation directory created",
58
+ errorTitle: "Docs Init Failed",
59
+ errorMsg: (_, out) => out.substring(0, 100),
60
+ },
61
+ cortex_init: {
62
+ successTitle: "Project Initialized",
63
+ successMsg: () => ".cortex directory created",
64
+ errorTitle: "Init Failed",
65
+ errorMsg: (_, out) => out.substring(0, 100),
66
+ },
67
+ cortex_configure: {
68
+ successTitle: "Models Configured",
69
+ successMsg: (args) => `Primary: ${args.primaryModel?.split("/").pop() || "set"}`,
70
+ errorTitle: "Configure Failed",
71
+ errorMsg: (_, out) => out.substring(0, 100),
72
+ },
73
+ branch_switch: {
74
+ successTitle: "Branch Switched",
75
+ successMsg: (args) => `Now on ${args.branch ?? "branch"}`,
76
+ errorTitle: "Branch Switch Failed",
77
+ errorMsg: (_, out) => out
78
+ .replace(/^✗\s*/, "")
79
+ .split("\n")[0]
80
+ .substring(0, 100),
81
+ },
82
+ };
83
+ // ─── Error Message Extraction ────────────────────────────────────────────────
84
+ //
85
+ // Extracts a human-readable message from the session error union type
86
+ // (ProviderAuthError | UnknownError | MessageOutputLengthError |
87
+ // MessageAbortedError | ApiError).
88
+ function extractErrorMessage(error) {
89
+ if (!error)
90
+ return "An unknown error occurred";
91
+ const msg = typeof error.data?.message === "string" ? error.data.message : "";
92
+ switch (error.name) {
93
+ case "ProviderAuthError":
94
+ return `Auth error: ${msg || "Provider authentication failed"}`;
95
+ case "UnknownError":
96
+ return msg || "An unknown error occurred";
97
+ case "MessageOutputLengthError":
98
+ return "Output length exceeded — try compacting the session";
99
+ case "MessageAbortedError":
100
+ return `Aborted: ${msg || "Message was aborted"}`;
101
+ case "APIError":
102
+ return `API error: ${msg || "Request failed"}`;
103
+ default:
104
+ return `Error: ${error.name}`;
105
+ }
106
+ }
107
+ // ─── Plugin Entry ────────────────────────────────────────────────────────────
19
108
  export const CortexPlugin = async (ctx) => {
20
109
  return {
21
110
  tool: {
22
111
  // Cortex tools - .cortex directory management
23
112
  cortex_init: cortex.init,
24
113
  cortex_status: cortex.status,
25
- // Worktree tools - git worktree management
26
- worktree_create: worktree.create,
114
+ cortex_configure: cortex.configure,
115
+ // Worktree tools - git worktree management (factories for toast notifications)
116
+ worktree_create: worktree.createCreate(ctx.client),
27
117
  worktree_list: worktree.list,
28
- worktree_remove: worktree.remove,
118
+ worktree_remove: worktree.createRemove(ctx.client),
29
119
  worktree_open: worktree.open,
30
- // Dynamic tool: needs client + shell via factory (closure injection)
31
120
  worktree_launch: worktree.createLaunch(ctx.client, ctx.$),
32
- // Branch tools - git branch operations
33
- branch_create: branch.create,
121
+ // Branch tools - git branch operations (factory for toast notifications)
122
+ branch_create: branch.createCreate(ctx.client),
34
123
  branch_status: branch.status,
35
124
  branch_switch: branch.switch_,
36
125
  // Plan tools - implementation plan persistence
@@ -50,9 +139,49 @@ export const CortexPlugin = async (ctx) => {
50
139
  // Task tools - finalize workflow (commit, push, PR)
51
140
  task_finalize: task.finalize,
52
141
  },
53
- // Agent handover toast notifications
142
+ // ── Post-execution toast notifications ────────────────────────────────
143
+ //
144
+ // Fires after every tool execution. Uses the TOOL_NOTIFICATIONS map
145
+ // to determine which tools get toasts and what content to display.
146
+ // Tools with existing factory-based toasts are excluded from the map.
147
+ "tool.execute.after": async (input, output) => {
148
+ const config = TOOL_NOTIFICATIONS[input.tool];
149
+ if (!config)
150
+ return; // No notification configured for this tool
151
+ try {
152
+ const result = output.output;
153
+ const isSuccess = result.startsWith("✓");
154
+ const isError = result.startsWith("✗");
155
+ if (isSuccess) {
156
+ await ctx.client.tui.showToast({
157
+ body: {
158
+ title: config.successTitle,
159
+ message: config.successMsg(input.args, result),
160
+ variant: "success",
161
+ duration: config.successDuration ?? 4000,
162
+ },
163
+ });
164
+ }
165
+ else if (isError) {
166
+ await ctx.client.tui.showToast({
167
+ body: {
168
+ title: config.errorTitle,
169
+ message: config.errorMsg(input.args, result),
170
+ variant: "error",
171
+ duration: config.errorDuration ?? 8000,
172
+ },
173
+ });
174
+ }
175
+ // Informational or warning outputs (⚠) — no toast to avoid noise
176
+ }
177
+ catch {
178
+ // Toast failure is non-fatal
179
+ }
180
+ },
181
+ // ── Event-driven notifications ───────────────────────────────────────
54
182
  async event({ event }) {
55
183
  try {
184
+ // Agent handover notifications
56
185
  if (event.type === "message.part.updated" &&
57
186
  "part" in event.properties &&
58
187
  event.properties.part.type === "agent") {
@@ -67,6 +196,43 @@ export const CortexPlugin = async (ctx) => {
67
196
  },
68
197
  });
69
198
  }
199
+ // Session error notifications
200
+ if (event.type === "session.error") {
201
+ const rawError = event.properties.error;
202
+ // Runtime validation before cast — ensure error has expected shape
203
+ const error = rawError &&
204
+ typeof rawError === "object" &&
205
+ "name" in rawError &&
206
+ typeof rawError.name === "string"
207
+ ? rawError
208
+ : undefined;
209
+ const message = extractErrorMessage(error);
210
+ await ctx.client.tui.showToast({
211
+ body: {
212
+ title: "Session Error",
213
+ message,
214
+ variant: "error",
215
+ duration: 10000,
216
+ },
217
+ });
218
+ }
219
+ // PTY exited notifications (relevant for worktree terminal sessions)
220
+ if (event.type === "pty.exited") {
221
+ const rawExitCode = event.properties.exitCode;
222
+ const exitCode = typeof rawExitCode === "number" && Number.isInteger(rawExitCode)
223
+ ? rawExitCode
224
+ : -1;
225
+ await ctx.client.tui.showToast({
226
+ body: {
227
+ title: "Terminal Exited",
228
+ message: exitCode === 0
229
+ ? "Terminal session completed successfully"
230
+ : `Terminal exited with code ${exitCode}`,
231
+ variant: exitCode === 0 ? "success" : "warning",
232
+ duration: exitCode === 0 ? 4000 : 8000,
233
+ },
234
+ });
235
+ }
70
236
  }
71
237
  catch {
72
238
  // Toast failure is non-fatal — silently ignore
@@ -19,11 +19,11 @@ export interface ModelEntry {
19
19
  }
20
20
  export declare const MODEL_REGISTRY: ModelEntry[];
21
21
  /** Primary agents receive the best available model */
22
- export declare const PRIMARY_AGENTS: readonly ["build", "plan", "debug"];
22
+ export declare const PRIMARY_AGENTS: readonly ["build", "plan", "debug", "review"];
23
23
  /** Subagents receive a fast/cost-effective model */
24
24
  export declare const SUBAGENTS: readonly ["fullstack", "testing", "security", "devops"];
25
25
  /** All agent names combined */
26
- export declare const ALL_AGENTS: readonly ["build", "plan", "debug", "fullstack", "testing", "security", "devops"];
26
+ export declare const ALL_AGENTS: readonly ["build", "plan", "debug", "review", "fullstack", "testing", "security", "devops"];
27
27
  /**
28
28
  * Build the interactive choices list for primary model selection.
29
29
  * Shows premium and standard tier models (excluding fast).
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IACtC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,EAAE,UAAU,EAuGtC,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,cAAc,qCAAsC,CAAC;AAElE,oDAAoD;AACpD,eAAO,MAAM,SAAS,yDAA0D,CAAC;AAEjF,+BAA+B;AAC/B,eAAO,MAAM,UAAU,mFAA6C,CAAC;AAErE;;;GAGG;AACH,wBAAgB,iBAAiB;;;;IAMhC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM;;;;IAcxD"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,IAAI,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,CAAC;IACtC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,cAAc,EAAE,UAAU,EAuGtC,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,cAAc,+CAAgD,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,SAAS,yDAA0D,CAAC;AAEjF,+BAA+B;AAC/B,eAAO,MAAM,UAAU,6FAA6C,CAAC;AAErE;;;GAGG;AACH,wBAAgB,iBAAiB;;;;IAMhC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM;;;;IAcxD"}
package/dist/registry.js CHANGED
@@ -105,7 +105,7 @@ export const MODEL_REGISTRY = [
105
105
  },
106
106
  ];
107
107
  /** Primary agents receive the best available model */
108
- export const PRIMARY_AGENTS = ["build", "plan", "debug"];
108
+ export const PRIMARY_AGENTS = ["build", "plan", "debug", "review"];
109
109
  /** Subagents receive a fast/cost-effective model */
110
110
  export const SUBAGENTS = ["fullstack", "testing", "security", "devops"];
111
111
  /** All agent names combined */
@@ -1,4 +1,10 @@
1
- export declare const create: {
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ type Client = PluginInput["client"];
3
+ /**
4
+ * Factory function that creates the branch_create tool with access
5
+ * to the OpenCode client for toast notifications.
6
+ */
7
+ export declare function createCreate(client: Client): {
2
8
  description: string;
3
9
  args: {
4
10
  name: import("zod").ZodString;
@@ -1 +1 @@
1
- {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/tools/branch.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;CA8CjB,CAAC;AAEH,eAAO,MAAM,MAAM;;;;CAiGjB,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;CAmClB,CAAC;AAGH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"branch.d.ts","sourceRoot":"","sources":["../../src/tools/branch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAMvD,KAAK,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;AAEpC;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;EA0E1C;AAED,eAAO,MAAM,MAAM;;;;CAkGjB,CAAC;AAEH,eAAO,MAAM,OAAO;;;;;;;;CAmClB,CAAC;AAGH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,CAAC"}
@@ -1,57 +1,91 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { git } from "../utils/shell.js";
2
3
  const PROTECTED_BRANCHES = ["main", "master", "develop", "production", "staging"];
3
- export const create = tool({
4
- description: "Create and checkout a new git branch with proper naming convention",
5
- args: {
6
- name: tool.schema
7
- .string()
8
- .describe("Branch name slug (e.g., 'user-authentication', 'fix-login')"),
9
- type: tool.schema
10
- .enum(["feature", "bugfix", "hotfix", "refactor", "docs", "test", "chore"])
11
- .describe("Branch type - determines prefix"),
12
- },
13
- async execute(args, context) {
14
- const { name, type } = args;
15
- const branchName = `${type}/${name}`;
16
- // Check if we're in a git repository
17
- try {
18
- await Bun.$ `git rev-parse --git-dir`.cwd(context.worktree).text();
19
- }
20
- catch {
21
- return "✗ Error: Not in a git repository";
22
- }
23
- // Check if branch already exists
24
- try {
25
- const branches = await Bun.$ `git branch --list ${branchName}`.cwd(context.worktree).text();
26
- if (branches.trim()) {
27
- return `✗ Error: Branch '${branchName}' already exists.
4
+ /**
5
+ * Factory function that creates the branch_create tool with access
6
+ * to the OpenCode client for toast notifications.
7
+ */
8
+ export function createCreate(client) {
9
+ return tool({
10
+ description: "Create and checkout a new git branch with proper naming convention",
11
+ args: {
12
+ name: tool.schema
13
+ .string()
14
+ .describe("Branch name slug (e.g., 'user-authentication', 'fix-login')"),
15
+ type: tool.schema
16
+ .enum(["feature", "bugfix", "hotfix", "refactor", "docs", "test", "chore"])
17
+ .describe("Branch type - determines prefix"),
18
+ },
19
+ async execute(args, context) {
20
+ const { name, type } = args;
21
+ const branchName = `${type}/${name}`;
22
+ // Check if we're in a git repository
23
+ try {
24
+ await git(context.worktree, "rev-parse", "--git-dir");
25
+ }
26
+ catch {
27
+ return "✗ Error: Not in a git repository";
28
+ }
29
+ // Check if branch already exists
30
+ try {
31
+ const { stdout } = await git(context.worktree, "branch", "--list", branchName);
32
+ if (stdout.trim()) {
33
+ return `✗ Error: Branch '${branchName}' already exists.
28
34
 
29
35
  Use branch_switch to switch to it, or choose a different name.`;
36
+ }
30
37
  }
31
- }
32
- catch {
33
- // Ignore error, branch check is optional
34
- }
35
- // Create and checkout the branch
36
- try {
37
- await Bun.$ `git checkout -b ${branchName}`.cwd(context.worktree);
38
- }
39
- catch (error) {
40
- return `✗ Error creating branch: ${error.message || error}`;
41
- }
42
- return `✓ Created and switched to branch: ${branchName}
38
+ catch {
39
+ // Ignore error, branch check is optional
40
+ }
41
+ // Create and checkout the branch
42
+ try {
43
+ await git(context.worktree, "checkout", "-b", branchName);
44
+ }
45
+ catch (error) {
46
+ try {
47
+ await client.tui.showToast({
48
+ body: {
49
+ title: `Branch: ${branchName}`,
50
+ message: `Failed to create: ${error.message || error}`,
51
+ variant: "error",
52
+ duration: 8000,
53
+ },
54
+ });
55
+ }
56
+ catch {
57
+ // Toast failure is non-fatal
58
+ }
59
+ return `✗ Error creating branch: ${error.message || error}`;
60
+ }
61
+ // Notify via toast
62
+ try {
63
+ await client.tui.showToast({
64
+ body: {
65
+ title: `Branch: ${branchName}`,
66
+ message: `Created and checked out`,
67
+ variant: "success",
68
+ duration: 4000,
69
+ },
70
+ });
71
+ }
72
+ catch {
73
+ // Toast failure is non-fatal
74
+ }
75
+ return `✓ Created and switched to branch: ${branchName}
43
76
 
44
77
  You are now on branch '${branchName}'.
45
78
  Make your changes and commit when ready.`;
46
- },
47
- });
79
+ },
80
+ });
81
+ }
48
82
  export const status = tool({
49
83
  description: "Get current git branch status - branch name, uncommitted changes, and whether on protected branch",
50
84
  args: {},
51
85
  async execute(args, context) {
52
86
  // Check if we're in a git repository
53
87
  try {
54
- await Bun.$ `git rev-parse --git-dir`.cwd(context.worktree).text();
88
+ await git(context.worktree, "rev-parse", "--git-dir");
55
89
  }
56
90
  catch {
57
91
  return "✗ Not in a git repository";
@@ -64,7 +98,8 @@ export const status = tool({
64
98
  let aheadBehind = "";
65
99
  // Get current branch
66
100
  try {
67
- currentBranch = (await Bun.$ `git branch --show-current`.cwd(context.worktree).text()).trim();
101
+ const { stdout } = await git(context.worktree, "branch", "--show-current");
102
+ currentBranch = stdout.trim();
68
103
  if (!currentBranch) {
69
104
  currentBranch = "(detached HEAD)";
70
105
  }
@@ -76,17 +111,17 @@ export const status = tool({
76
111
  isProtected = PROTECTED_BRANCHES.includes(currentBranch);
77
112
  // Check for changes
78
113
  try {
79
- const statusOutput = await Bun.$ `git status --porcelain`.cwd(context.worktree).text();
80
- const lines = statusOutput.trim().split("\n").filter((l) => l);
114
+ const { stdout } = await git(context.worktree, "status", "--porcelain");
115
+ const lines = stdout.trim().split("\n").filter((l) => l);
81
116
  for (const line of lines) {
82
- const status = line.substring(0, 2);
83
- if (status[0] !== " " && status[0] !== "?") {
117
+ const st = line.substring(0, 2);
118
+ if (st[0] !== " " && st[0] !== "?") {
84
119
  stagedChanges = true;
85
120
  }
86
- if (status[1] !== " " && status[1] !== "?") {
121
+ if (st[1] !== " " && st[1] !== "?") {
87
122
  hasChanges = true;
88
123
  }
89
- if (status === "??") {
124
+ if (st === "??") {
90
125
  untrackedFiles = true;
91
126
  }
92
127
  }
@@ -96,8 +131,8 @@ export const status = tool({
96
131
  }
97
132
  // Check ahead/behind
98
133
  try {
99
- const result = await Bun.$ `git rev-list --left-right --count HEAD...@{upstream}`.cwd(context.worktree).text();
100
- const [ahead, behind] = result.trim().split(/\s+/);
134
+ const { stdout } = await git(context.worktree, "rev-list", "--left-right", "--count", "HEAD...@{upstream}");
135
+ const [ahead, behind] = stdout.trim().split(/\s+/);
101
136
  if (parseInt(ahead) > 0 || parseInt(behind) > 0) {
102
137
  aheadBehind = `Ahead: ${ahead}, Behind: ${behind}`;
103
138
  }
@@ -146,10 +181,10 @@ export const switch_ = tool({
146
181
  const { branch } = args;
147
182
  // Check if branch exists
148
183
  try {
149
- const branches = await Bun.$ `git branch --list ${branch}`.cwd(context.worktree).text();
150
- if (!branches.trim()) {
184
+ const { stdout } = await git(context.worktree, "branch", "--list", branch);
185
+ if (!stdout.trim()) {
151
186
  // Try remote branch
152
- const remoteBranches = await Bun.$ `git branch -r --list origin/${branch}`.cwd(context.worktree).text();
187
+ const { stdout: remoteBranches } = await git(context.worktree, "branch", "-r", "--list", `origin/${branch}`);
153
188
  if (!remoteBranches.trim()) {
154
189
  return `✗ Error: Branch '${branch}' not found locally or on origin.
155
190
 
@@ -162,7 +197,7 @@ Use branch_create to create a new branch.`;
162
197
  }
163
198
  // Switch to branch
164
199
  try {
165
- await Bun.$ `git checkout ${branch}`.cwd(context.worktree);
200
+ await git(context.worktree, "checkout", branch);
166
201
  }
167
202
  catch (error) {
168
203
  return `✗ Error switching branch: ${error.message || error}
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  export declare const init: {
2
3
  description: string;
3
4
  args: {};
@@ -8,4 +9,22 @@ export declare const status: {
8
9
  args: {};
9
10
  execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
10
11
  };
12
+ /**
13
+ * cortex_configure — Write per-project model configuration to ./opencode.json.
14
+ *
15
+ * Accepts a primary model (for build/plan/debug) and a subagent model
16
+ * (for fullstack/testing/security/devops). Merges into any existing
17
+ * opencode.json at the project root, preserving other settings.
18
+ */
19
+ export declare const configure: {
20
+ description: string;
21
+ args: {
22
+ primaryModel: z.ZodString;
23
+ subagentModel: z.ZodString;
24
+ };
25
+ execute(args: {
26
+ primaryModel: string;
27
+ subagentModel: string;
28
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
29
+ };
11
30
  //# sourceMappingURL=cortex.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cortex.d.ts","sourceRoot":"","sources":["../../src/tools/cortex.ts"],"names":[],"mappings":"AAkEA,eAAO,MAAM,IAAI;;;;CAkDf,CAAC;AAEH,eAAO,MAAM,MAAM;;;;CAyDjB,CAAC"}
1
+ {"version":3,"file":"cortex.d.ts","sourceRoot":"","sources":["../../src/tools/cortex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqExB,eAAO,MAAM,IAAI;;;;CAkDf,CAAC;AAEH,eAAO,MAAM,MAAM;;;;CAyDjB,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;CAkHpB,CAAC"}
@@ -1,6 +1,8 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
+ import { z } from "zod";
5
+ import { PRIMARY_AGENTS, SUBAGENTS, MODEL_REGISTRY, } from "../registry.js";
4
6
  const CORTEX_DIR = ".cortex";
5
7
  const DEFAULT_CONFIG = {
6
8
  version: "1.0.0",
@@ -146,3 +148,110 @@ Plans: ${planCount}`;
146
148
  return output;
147
149
  },
148
150
  });
151
+ /**
152
+ * cortex_configure — Write per-project model configuration to ./opencode.json.
153
+ *
154
+ * Accepts a primary model (for build/plan/debug) and a subagent model
155
+ * (for fullstack/testing/security/devops). Merges into any existing
156
+ * opencode.json at the project root, preserving other settings.
157
+ */
158
+ export const configure = tool({
159
+ description: "Save per-project model configuration to ./opencode.json. " +
160
+ "Sets the model for primary agents (build, plan, debug) and subagents (fullstack, testing, security, devops). " +
161
+ "Available models — Premium: " +
162
+ MODEL_REGISTRY.filter((m) => m.tier === "premium")
163
+ .map((m) => `${m.name} (${m.id})`)
164
+ .join(", ") +
165
+ ". Standard: " +
166
+ MODEL_REGISTRY.filter((m) => m.tier === "standard")
167
+ .map((m) => `${m.name} (${m.id})`)
168
+ .join(", ") +
169
+ ". Fast: " +
170
+ MODEL_REGISTRY.filter((m) => m.tier === "fast")
171
+ .map((m) => `${m.name} (${m.id})`)
172
+ .join(", ") +
173
+ ".",
174
+ args: {
175
+ primaryModel: z
176
+ .string()
177
+ .describe("Model ID for primary agents (build, plan, debug). Format: provider/model-name"),
178
+ subagentModel: z
179
+ .string()
180
+ .describe("Model ID for subagents (fullstack, testing, security, devops). Format: provider/model-name"),
181
+ },
182
+ async execute(args, context) {
183
+ const configPath = path.join(context.worktree, "opencode.json");
184
+ // Read existing config or start fresh
185
+ let config = {};
186
+ if (fs.existsSync(configPath)) {
187
+ try {
188
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
189
+ }
190
+ catch {
191
+ // Malformed JSON — start fresh but warn
192
+ config = {};
193
+ }
194
+ }
195
+ // Ensure schema and plugin
196
+ if (!config.$schema) {
197
+ config.$schema = "https://opencode.ai/config.json";
198
+ }
199
+ const plugins = config.plugin ?? [];
200
+ if (!plugins.includes("cortex-agents")) {
201
+ plugins.push("cortex-agents");
202
+ }
203
+ config.plugin = plugins;
204
+ // Build agent config
205
+ const agent = config.agent ?? {};
206
+ for (const name of PRIMARY_AGENTS) {
207
+ if (!agent[name])
208
+ agent[name] = {};
209
+ agent[name].model = args.primaryModel;
210
+ }
211
+ for (const name of SUBAGENTS) {
212
+ if (!agent[name])
213
+ agent[name] = {};
214
+ agent[name].model = args.subagentModel;
215
+ }
216
+ config.agent = agent;
217
+ // Write to project root (runtime config — what OpenCode reads)
218
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
219
+ // Also write .opencode/models.json (per-project source of truth, version controlled)
220
+ const modelsPath = path.join(context.worktree, ".opencode", "models.json");
221
+ const modelsDir = path.dirname(modelsPath);
222
+ if (fs.existsSync(modelsDir)) {
223
+ const modelsConfig = {
224
+ primary: { model: args.primaryModel },
225
+ subagent: { model: args.subagentModel },
226
+ agents: {},
227
+ };
228
+ const agentsMap = {};
229
+ for (const name of PRIMARY_AGENTS) {
230
+ agentsMap[name] = { model: args.primaryModel };
231
+ }
232
+ for (const name of SUBAGENTS) {
233
+ agentsMap[name] = { model: args.subagentModel };
234
+ }
235
+ modelsConfig.agents = agentsMap;
236
+ fs.writeFileSync(modelsPath, JSON.stringify(modelsConfig, null, 2) + "\n");
237
+ }
238
+ // Build summary
239
+ const primaryDisplay = MODEL_REGISTRY.find((m) => m.id === args.primaryModel)?.name ??
240
+ args.primaryModel;
241
+ const subagentDisplay = MODEL_REGISTRY.find((m) => m.id === args.subagentModel)?.name ??
242
+ args.subagentModel;
243
+ const savedTo = fs.existsSync(modelsDir)
244
+ ? `${configPath}\n .opencode/models.json`
245
+ : configPath;
246
+ return `✓ Model configuration saved to:
247
+ ${savedTo}
248
+
249
+ Primary agents (build, plan, debug):
250
+ → ${primaryDisplay} (${args.primaryModel})
251
+
252
+ Subagents (fullstack, testing, security, devops):
253
+ → ${subagentDisplay} (${args.subagentModel})
254
+
255
+ Restart OpenCode to apply changes.`;
256
+ },
257
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;CAsFf,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CA8Df,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CAuBf,CAAC"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/tools/session.ts"],"names":[],"mappings":"AA4BA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;CAuFf,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CA8Df,CAAC;AAEH,eAAO,MAAM,IAAI;;;;;;;;CAuBf,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
+ import { git } from "../utils/shell.js";
4
5
  const CORTEX_DIR = ".cortex";
5
6
  const SESSIONS_DIR = "sessions";
6
7
  function getDatePrefix() {
@@ -51,7 +52,8 @@ export const save = tool({
51
52
  let currentBranch = branch;
52
53
  if (!currentBranch) {
53
54
  try {
54
- currentBranch = (await Bun.$ `git branch --show-current`.cwd(context.worktree).text()).trim();
55
+ const { stdout } = await git(context.worktree, "branch", "--show-current");
56
+ currentBranch = stdout.trim();
55
57
  }
56
58
  catch {
57
59
  currentBranch = "unknown";
@@ -1 +1 @@
1
- {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AAyGA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;CA0OnB,CAAC"}
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/tools/task.ts"],"names":[],"mappings":"AAyGA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;CAqNnB,CAAC"}