cursor-opencode-provider 0.7.6 → 0.8.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/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"LICENSE",
|
|
71
71
|
"DISCLAIMER.md",
|
|
72
72
|
"SECURITY.md",
|
|
73
|
-
"README.md"
|
|
73
|
+
"README.md",
|
|
74
|
+
"CHANGELOG.md"
|
|
74
75
|
],
|
|
75
76
|
"scripts": {
|
|
76
77
|
"build": "tsc",
|
package/dist/context/agents.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export type CollectedAgent = {
|
|
2
|
-
fullPath: string;
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
prompt: string;
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* Custom agents from the active OpenCode-compatible config roots. A host bridge may
|
|
9
|
-
* install a host-neutral bridge before loading this module; without one this
|
|
10
|
-
* remains the direct OpenCode discovery path.
|
|
11
|
-
*/
|
|
12
|
-
export declare function collectAgents(workspaceRoot: string): Promise<CollectedAgent[]>;
|
package/dist/context/agents.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
|
|
4
|
-
async function exists(p) {
|
|
5
|
-
try {
|
|
6
|
-
await stat(p);
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
function parseAgentMarkdown(raw, fallbackName) {
|
|
14
|
-
let name = fallbackName;
|
|
15
|
-
let description = "";
|
|
16
|
-
let body = raw;
|
|
17
|
-
if (raw.startsWith("---")) {
|
|
18
|
-
const end = raw.indexOf("\n---", 3);
|
|
19
|
-
if (end >= 0) {
|
|
20
|
-
const fm = raw.slice(3, end).trim();
|
|
21
|
-
body = raw.slice(end + 4).replace(/^\n/, "");
|
|
22
|
-
for (const line of fm.split("\n")) {
|
|
23
|
-
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
24
|
-
if (!m)
|
|
25
|
-
continue;
|
|
26
|
-
let val = m[2].trim();
|
|
27
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
28
|
-
val = val.slice(1, -1);
|
|
29
|
-
}
|
|
30
|
-
if (m[1] === "name")
|
|
31
|
-
name = val;
|
|
32
|
-
if (m[1] === "description")
|
|
33
|
-
description = val;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return { name, description, prompt: body.slice(0, 20_000) };
|
|
38
|
-
}
|
|
39
|
-
function uniquePaths(paths) {
|
|
40
|
-
const seen = new Set();
|
|
41
|
-
return paths.filter((value) => {
|
|
42
|
-
const normalized = path.resolve(value);
|
|
43
|
-
if (seen.has(normalized))
|
|
44
|
-
return false;
|
|
45
|
-
seen.add(normalized);
|
|
46
|
-
return true;
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Read one host config root's `agent/` and `agents/` trees. Host loaders use
|
|
51
|
-
* both spellings and recurse through nested markdown files. Directory symlinks
|
|
52
|
-
* are followed with realpath cycle detection, matching host behavior without
|
|
53
|
-
* allowing a symlink loop to hang context construction.
|
|
54
|
-
*/
|
|
55
|
-
async function scanAgentRoot(root, out) {
|
|
56
|
-
if (!(await exists(root)))
|
|
57
|
-
return;
|
|
58
|
-
const visitedDirs = new Set();
|
|
59
|
-
const scanDir = async (dir, relativePrefix) => {
|
|
60
|
-
let canonical;
|
|
61
|
-
try {
|
|
62
|
-
canonical = await realpath(dir);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (visitedDirs.has(canonical))
|
|
68
|
-
return;
|
|
69
|
-
visitedDirs.add(canonical);
|
|
70
|
-
let entries;
|
|
71
|
-
try {
|
|
72
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
73
|
-
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
for (const entry of entries) {
|
|
79
|
-
const full = path.join(dir, entry.name);
|
|
80
|
-
const relative = path.join(relativePrefix, entry.name);
|
|
81
|
-
let isDirectory = entry.isDirectory();
|
|
82
|
-
let isFile = entry.isFile();
|
|
83
|
-
if (entry.isSymbolicLink()) {
|
|
84
|
-
try {
|
|
85
|
-
const target = await stat(full);
|
|
86
|
-
isDirectory = target.isDirectory();
|
|
87
|
-
isFile = target.isFile();
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (isDirectory) {
|
|
94
|
-
await scanDir(full, relative);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (!isFile || !entry.name.endsWith(".md"))
|
|
98
|
-
continue;
|
|
99
|
-
try {
|
|
100
|
-
const raw = await readFile(full, "utf-8");
|
|
101
|
-
const fallbackName = relative.replace(/\\/g, "/").replace(/\.md$/, "");
|
|
102
|
-
const parsed = parseAgentMarkdown(raw, fallbackName);
|
|
103
|
-
if (!out.has(parsed.name)) {
|
|
104
|
-
out.set(parsed.name, {
|
|
105
|
-
fullPath: path.resolve(full),
|
|
106
|
-
name: parsed.name,
|
|
107
|
-
description: parsed.description,
|
|
108
|
-
prompt: parsed.prompt,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
/* skip unreadable or malformed files without failing context build */
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
// Scan singular before plural, then let root ordering establish host
|
|
118
|
-
// precedence. This is deterministic and mirrors the hosts' brace glob.
|
|
119
|
-
await scanDir(path.join(root, "agent"), "agent");
|
|
120
|
-
await scanDir(path.join(root, "agents"), "agents");
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Custom agents from the active OpenCode-compatible config roots. A host bridge may
|
|
124
|
-
* install a host-neutral bridge before loading this module; without one this
|
|
125
|
-
* remains the direct OpenCode discovery path.
|
|
126
|
-
*/
|
|
127
|
-
export async function collectAgents(workspaceRoot) {
|
|
128
|
-
const out = new Map();
|
|
129
|
-
const roots = uniquePaths([
|
|
130
|
-
...opencodeProjectConfigDirs(workspaceRoot),
|
|
131
|
-
...opencodeGlobalConfigDirs(),
|
|
132
|
-
]);
|
|
133
|
-
for (const root of roots)
|
|
134
|
-
await scanAgentRoot(root, out);
|
|
135
|
-
return [...out.values()];
|
|
136
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Queue OpenCode's post-plan-exit kickoff after CreatePlan approval.
|
|
3
|
-
*
|
|
4
|
-
* Native `plan_exit` asks the user, then injects a synthetic user message with
|
|
5
|
-
* `agent: "build"` so a new turn starts implementing. Our CreatePlan → question
|
|
6
|
-
* path already asks; this module is the missing second half. A host plugin
|
|
7
|
-
* installs the prompt handler; the language model records a request after Yes,
|
|
8
|
-
* and the outer stream wrapper flushes it only after the current `doStream`
|
|
9
|
-
* result stream has settled. This prevents a second generation from starting
|
|
10
|
-
* while the continuation Run is still live.
|
|
11
|
-
*/
|
|
12
|
-
export type PlanExecutionKickoffInput = {
|
|
13
|
-
sessionID: string;
|
|
14
|
-
/** Filesystem path (or relative display path) of the approved plan. */
|
|
15
|
-
planPath: string;
|
|
16
|
-
/** Concrete Cursor Run that owns the approval. Guards recovery/supersession. */
|
|
17
|
-
cursorSessionID?: string;
|
|
18
|
-
};
|
|
19
|
-
export type PlanExecutionKickoffFn = (input: PlanExecutionKickoffInput) => void | Promise<void>;
|
|
20
|
-
/** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */
|
|
21
|
-
export declare function createPlanExecutionKickoffText(planPath: string): string;
|
|
22
|
-
/**
|
|
23
|
-
* Prefer a worktree-relative label when the plan sits under `workspaceRoot`,
|
|
24
|
-
* matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`.
|
|
25
|
-
* Otherwise keep the absolute path (global data `plans/` is outside the tree).
|
|
26
|
-
*/
|
|
27
|
-
export declare function formatPlanKickoffPath(planPath: string, workspaceRoot?: string): string;
|
|
28
|
-
/** Decode a `file://` plan URI to a filesystem path; pass other strings through. */
|
|
29
|
-
export declare function planPathFromUri(planUri: string): string;
|
|
30
|
-
export type PlanExecutionKickoffState = PlanExecutionKickoffInput & {
|
|
31
|
-
status: "pending" | "failed";
|
|
32
|
-
attempts: number;
|
|
33
|
-
lastError?: string;
|
|
34
|
-
};
|
|
35
|
-
/** Install (or clear) the host kickoff. */
|
|
36
|
-
export declare function setPlanExecutionKickoff(fn: PlanExecutionKickoffFn | undefined): void;
|
|
37
|
-
export declare function hasPlanExecutionKickoff(): boolean;
|
|
38
|
-
/** Record the approved plan; execution is flushed after the stream settles. */
|
|
39
|
-
export declare function queuePlanExecutionKickoff(input: PlanExecutionKickoffInput): boolean;
|
|
40
|
-
/** Drop any approval state owned by a deleted host session. */
|
|
41
|
-
export declare function cancelPlanExecutionKickoff(sessionID: string | undefined): void;
|
|
42
|
-
/**
|
|
43
|
-
* Run one pending kickoff only after the owning Cursor Run reached a terminal,
|
|
44
|
-
* idle state. Returns true only after successful host handoff.
|
|
45
|
-
*/
|
|
46
|
-
export declare function flushPlanExecutionKickoff(sessionID: string | undefined, options?: {
|
|
47
|
-
cursorSessionID?: string;
|
|
48
|
-
terminal?: boolean;
|
|
49
|
-
pumpActive?: boolean;
|
|
50
|
-
pendingExecs?: number;
|
|
51
|
-
}): Promise<boolean>;
|
|
52
|
-
/** Consume a user-visible warning on the next provider turn. */
|
|
53
|
-
export declare function takePlanExecutionKickoffWarning(sessionID: string | undefined): string | undefined;
|
|
54
|
-
/** Retry state remains explicit; no timer or automatic loop fires it. */
|
|
55
|
-
export declare function planExecutionKickoffState(sessionID: string | undefined): PlanExecutionKickoffState | undefined;
|
|
56
|
-
/** Test helper: clear the registered kickoff and pending approvals. */
|
|
57
|
-
export declare function resetPlanExecutionKickoffForTests(): void;
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Queue OpenCode's post-plan-exit kickoff after CreatePlan approval.
|
|
3
|
-
*
|
|
4
|
-
* Native `plan_exit` asks the user, then injects a synthetic user message with
|
|
5
|
-
* `agent: "build"` so a new turn starts implementing. Our CreatePlan → question
|
|
6
|
-
* path already asks; this module is the missing second half. A host plugin
|
|
7
|
-
* installs the prompt handler; the language model records a request after Yes,
|
|
8
|
-
* and the outer stream wrapper flushes it only after the current `doStream`
|
|
9
|
-
* result stream has settled. This prevents a second generation from starting
|
|
10
|
-
* while the continuation Run is still live.
|
|
11
|
-
*/
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { trace } from "./debug.js";
|
|
15
|
-
/** Upstream `PlanExitTool` wording — keep verbatim so behaviour matches OpenCode. */
|
|
16
|
-
export function createPlanExecutionKickoffText(planPath) {
|
|
17
|
-
const where = planPath.trim() || "the plan";
|
|
18
|
-
return `The plan at ${where} has been approved, you can now edit files. Execute the plan`;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Prefer a worktree-relative label when the plan sits under `workspaceRoot`,
|
|
22
|
-
* matching OpenCode's `path.relative(instance.worktree, Session.plan(...))`.
|
|
23
|
-
* Otherwise keep the absolute path (global data `plans/` is outside the tree).
|
|
24
|
-
*/
|
|
25
|
-
export function formatPlanKickoffPath(planPath, workspaceRoot) {
|
|
26
|
-
const absolute = planPath.trim();
|
|
27
|
-
if (!absolute)
|
|
28
|
-
return absolute;
|
|
29
|
-
const root = workspaceRoot?.trim();
|
|
30
|
-
if (!root)
|
|
31
|
-
return absolute;
|
|
32
|
-
const relative = path.relative(root, absolute);
|
|
33
|
-
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
34
|
-
return absolute;
|
|
35
|
-
}
|
|
36
|
-
return relative;
|
|
37
|
-
}
|
|
38
|
-
/** Decode a `file://` plan URI to a filesystem path; pass other strings through. */
|
|
39
|
-
export function planPathFromUri(planUri) {
|
|
40
|
-
const raw = planUri.trim();
|
|
41
|
-
if (!raw)
|
|
42
|
-
return "";
|
|
43
|
-
if (raw.startsWith("file:")) {
|
|
44
|
-
try {
|
|
45
|
-
return fileURLToPath(raw);
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return raw;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return raw;
|
|
52
|
-
}
|
|
53
|
-
let kickoff;
|
|
54
|
-
const pending = new Map();
|
|
55
|
-
const warnings = new Map();
|
|
56
|
-
const MAX_PENDING_PLAN_KICKOFFS = 256;
|
|
57
|
-
/** Install (or clear) the host kickoff. */
|
|
58
|
-
export function setPlanExecutionKickoff(fn) {
|
|
59
|
-
kickoff = fn;
|
|
60
|
-
if (!fn) {
|
|
61
|
-
pending.clear();
|
|
62
|
-
warnings.clear();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
export function hasPlanExecutionKickoff() {
|
|
66
|
-
return kickoff !== undefined;
|
|
67
|
-
}
|
|
68
|
-
/** Record the approved plan; execution is flushed after the stream settles. */
|
|
69
|
-
export function queuePlanExecutionKickoff(input) {
|
|
70
|
-
const sessionID = input.sessionID.trim();
|
|
71
|
-
const planPath = input.planPath.trim();
|
|
72
|
-
if (!sessionID || !planPath || !kickoff) {
|
|
73
|
-
trace(`plan-execution-kickoff: not queued sessionID=${JSON.stringify(sessionID)} ` +
|
|
74
|
-
`planPath=${JSON.stringify(planPath)} handler=${Boolean(kickoff)}`);
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
pending.set(sessionID, {
|
|
78
|
-
sessionID,
|
|
79
|
-
planPath,
|
|
80
|
-
...(input.cursorSessionID ? { cursorSessionID: input.cursorSessionID } : {}),
|
|
81
|
-
status: "pending",
|
|
82
|
-
attempts: 0,
|
|
83
|
-
});
|
|
84
|
-
warnings.delete(sessionID);
|
|
85
|
-
while (pending.size > MAX_PENDING_PLAN_KICKOFFS) {
|
|
86
|
-
const oldest = pending.keys().next().value;
|
|
87
|
-
if (!oldest)
|
|
88
|
-
break;
|
|
89
|
-
pending.delete(oldest);
|
|
90
|
-
warnings.delete(oldest);
|
|
91
|
-
}
|
|
92
|
-
trace(`plan-execution-kickoff: pending sessionID=${sessionID} ` +
|
|
93
|
-
`cursorSessionID=${input.cursorSessionID ?? ""} planPath=${planPath}`);
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
/** Drop any approval state owned by a deleted host session. */
|
|
97
|
-
export function cancelPlanExecutionKickoff(sessionID) {
|
|
98
|
-
const key = sessionID?.trim();
|
|
99
|
-
if (!key)
|
|
100
|
-
return;
|
|
101
|
-
pending.delete(key);
|
|
102
|
-
warnings.delete(key);
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Run one pending kickoff only after the owning Cursor Run reached a terminal,
|
|
106
|
-
* idle state. Returns true only after successful host handoff.
|
|
107
|
-
*/
|
|
108
|
-
export async function flushPlanExecutionKickoff(sessionID, options = {}) {
|
|
109
|
-
const key = sessionID?.trim();
|
|
110
|
-
if (!key)
|
|
111
|
-
return false;
|
|
112
|
-
const state = pending.get(key);
|
|
113
|
-
if (!state)
|
|
114
|
-
return false;
|
|
115
|
-
if (options.terminal !== true || options.pumpActive || (options.pendingExecs ?? 0) > 0) {
|
|
116
|
-
trace(`plan-execution-kickoff: deferred sessionID=${key} terminal=${Boolean(options.terminal)} ` +
|
|
117
|
-
`pumpActive=${Boolean(options.pumpActive)} pending=${options.pendingExecs ?? 0}`);
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
if (state.cursorSessionID
|
|
121
|
-
&& options.cursorSessionID
|
|
122
|
-
&& state.cursorSessionID !== options.cursorSessionID) {
|
|
123
|
-
// The owning Run was superseded before the handoff could happen. Applying
|
|
124
|
-
// it from a later Run would execute a stale approval in the wrong turn.
|
|
125
|
-
pending.delete(key);
|
|
126
|
-
warnings.delete(key);
|
|
127
|
-
trace(`plan-execution-kickoff: discarded stale Run sessionID=${key} ` +
|
|
128
|
-
`owner=${state.cursorSessionID} terminal=${options.cursorSessionID}`);
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
const run = kickoff;
|
|
132
|
-
if (!run) {
|
|
133
|
-
const message = "The plan was approved, but this host cannot start its execution turn.";
|
|
134
|
-
state.status = "failed";
|
|
135
|
-
state.lastError = message;
|
|
136
|
-
warnings.set(key, message);
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
state.attempts += 1;
|
|
140
|
-
try {
|
|
141
|
-
await run({
|
|
142
|
-
sessionID: state.sessionID,
|
|
143
|
-
planPath: state.planPath,
|
|
144
|
-
...(state.cursorSessionID ? { cursorSessionID: state.cursorSessionID } : {}),
|
|
145
|
-
});
|
|
146
|
-
pending.delete(key);
|
|
147
|
-
warnings.delete(key);
|
|
148
|
-
trace(`plan-execution-kickoff: queued sessionID=${key} planPath=${state.planPath}`);
|
|
149
|
-
return true;
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
153
|
-
const message = `The approved plan could not start execution: ${detail}. ` +
|
|
154
|
-
"The plan remains active and can be retried.";
|
|
155
|
-
state.status = "failed";
|
|
156
|
-
state.lastError = detail;
|
|
157
|
-
// A later explicit provider turn may retry after its own terminal boundary;
|
|
158
|
-
// do not keep pinning the retry to the now-finished Cursor Run.
|
|
159
|
-
delete state.cursorSessionID;
|
|
160
|
-
warnings.set(key, message);
|
|
161
|
-
trace(`plan-execution-kickoff: FAILED sessionID=${key} attempts=${state.attempts} err=${detail}`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/** Consume a user-visible warning on the next provider turn. */
|
|
166
|
-
export function takePlanExecutionKickoffWarning(sessionID) {
|
|
167
|
-
const key = sessionID?.trim();
|
|
168
|
-
if (!key)
|
|
169
|
-
return undefined;
|
|
170
|
-
const message = warnings.get(key);
|
|
171
|
-
if (message)
|
|
172
|
-
warnings.delete(key);
|
|
173
|
-
return message;
|
|
174
|
-
}
|
|
175
|
-
/** Retry state remains explicit; no timer or automatic loop fires it. */
|
|
176
|
-
export function planExecutionKickoffState(sessionID) {
|
|
177
|
-
const key = sessionID?.trim();
|
|
178
|
-
const state = key ? pending.get(key) : undefined;
|
|
179
|
-
return state ? { ...state } : undefined;
|
|
180
|
-
}
|
|
181
|
-
/** Test helper: clear the registered kickoff and pending approvals. */
|
|
182
|
-
export function resetPlanExecutionKickoffForTests() {
|
|
183
|
-
kickoff = undefined;
|
|
184
|
-
pending.clear();
|
|
185
|
-
warnings.clear();
|
|
186
|
-
}
|