@diffexai/diffex 0.2.1 → 0.2.3
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 +13 -1
- package/README.md +5 -725
- package/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/AGENTS.md +28 -0
- package/dist/cli/session-selection.d.ts +7 -0
- package/dist/cli/session-selection.js +181 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -0
- package/dist/core/extensions/factory.d.ts +21 -0
- package/dist/core/extensions/factory.js +254 -0
- package/dist/core/extensions/loader.d.ts +2 -10
- package/dist/core/extensions/loader.js +2 -251
- package/dist/core/prompt-templates.js +6 -1
- package/dist/core/resource-loader.js +7 -2
- package/dist/core/system-prompt.js +9 -8
- package/dist/core/tools/subagents.js +1 -0
- package/dist/main.js +3 -176
- package/dist/node_modules/@diffexai/diffex-agent-core/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-components.json +4 -4
- package/dist/node_modules/@diffexai/diffex-agent-core/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-agent-core/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-ai/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-ai/dist/cli.js +0 -0
- package/dist/node_modules/@diffexai/diffex-ai/dist/image-models.generated.d.ts +180 -0
- package/dist/node_modules/@diffexai/diffex-ai/dist/image-models.generated.js +182 -2
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/.manifest.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/kimi-coding.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/nvidia.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/openrouter.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/qwen-token-plan-cn.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/node_modules/@diffexai/diffex-ai/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-ai/distribution-files.json +9 -9
- package/dist/node_modules/@diffexai/diffex-ai/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-client/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-client/distribution-components.json +3 -3
- package/dist/node_modules/@diffexai/diffex-client/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-client/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-harness-state/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-harness-state/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-harness-state/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-protocol/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-protocol/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-protocol/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-telemetry/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-telemetry/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-telemetry/package.json +1 -6
- package/dist/node_modules/@diffexai/diffex-tui/THIRD-PARTY-NOTICES.txt +6 -2
- package/dist/node_modules/@diffexai/diffex-tui/distribution-components.json +2 -2
- package/dist/node_modules/@diffexai/diffex-tui/distribution-files.json +1 -1
- package/dist/node_modules/@diffexai/diffex-tui/package.json +1 -6
- package/distribution-components.json +13 -13
- package/distribution-files.json +66 -74
- package/docs/installation.md +118 -0
- package/docs/quickstart.md +7 -24
- package/docs/usage.md +10 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +36 -10
- package/release/distribution-manifest.json +7 -6
- package/release/install-package-lock.json +5 -5
- package/release/install-package.json +2 -2
- package/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-agent-core/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-ai/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-client/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-harness-state/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-protocol/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-telemetry/NOTICE +0 -6
- package/dist/node_modules/@diffexai/diffex-tui/NOTICE +0 -6
- package/docs/development.md +0 -71
- package/docs/docs.json +0 -160
- package/docs/images/doom-extension.png +0 -0
- package/docs/images/exy.png +0 -0
- package/docs/index.md +0 -78
- package/docs/invocation-traces.md +0 -82
- package/docs/termux.md +0 -127
- package/docs/windows.md +0 -17
package/THIRD-PARTY-NOTICES.txt
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
Diffex
|
|
2
|
-
|
|
1
|
+
Diffex
|
|
2
|
+
Copyright 2026 Luntraa, Inc.
|
|
3
|
+
|
|
4
|
+
Where this distribution contains Pi Agent Harness-derived portions,
|
|
5
|
+
those portions retain their MIT terms and copyright notice in LICENSE.
|
|
6
|
+
The Apache-2.0 package declaration does not replace third-party terms.
|
|
3
7
|
|
|
4
8
|
===== highlight.js@11.9.0 (BSD-3-Clause) =====
|
|
5
9
|
BSD 3-Clause License
|
package/dist/AGENTS.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Development Rules
|
|
2
|
+
|
|
3
|
+
## Conversational Style
|
|
4
|
+
|
|
5
|
+
- Keep responses concise and direct, adapting detail and terminology to the user.
|
|
6
|
+
- No emojis in commits, issues, PR comments, or code.
|
|
7
|
+
- No fluff or cheerful filler text (e.g., "Thanks @user" not "Thanks so much @user!").
|
|
8
|
+
- When the user asks a question, answer it first before making edits or running implementation commands.
|
|
9
|
+
- Address the user's feedback directly before describing changes.
|
|
10
|
+
|
|
11
|
+
## Code Quality
|
|
12
|
+
|
|
13
|
+
- Follow repository instructions, conventions, and existing tooling.
|
|
14
|
+
- Before editing or drawing conclusions, read enough surrounding code to understand the relevant behavior and dependencies.
|
|
15
|
+
- Read affected files in full for broad changes; do not rely on search snippets alone for investigations or audits.
|
|
16
|
+
- Ask before removing intentional functionality or code when the removal falls outside the requested scope or its purpose is unclear.
|
|
17
|
+
- Preserve unrelated local changes and keep modifications focused on the requested task.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
- After changes, run relevant tests and checks, including any tests you create or modify.
|
|
22
|
+
- Resolve failures caused by your changes and report any remaining failures or checks you could not run.
|
|
23
|
+
- Write substantial or multiline ad-hoc scripts to a temporary file, run and revise them as needed, and remove your temporary files when done.
|
|
24
|
+
- Simple one-line scripts may run directly in the shell.
|
|
25
|
+
|
|
26
|
+
## User Override
|
|
27
|
+
|
|
28
|
+
- Explicit user instructions take precedence over these defaults.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SessionManager } from "../core/session-manager.ts";
|
|
2
|
+
import type { SettingsManager } from "../core/settings-manager.ts";
|
|
3
|
+
import type { Args } from "./args.ts";
|
|
4
|
+
export declare function validateForkFlags(parsed: Args): void;
|
|
5
|
+
export declare function validateSessionIdFlags(parsed: Args): void;
|
|
6
|
+
export declare function createSessionManager(parsed: Args, cwd: string, sessionDir: string | undefined, settingsManager: SettingsManager): Promise<SessionManager>;
|
|
7
|
+
export declare function applySessionName(parsed: Args, sessionManager: SessionManager): void;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { assertValidSessionId, SessionManager } from "../core/session-manager.js";
|
|
4
|
+
import { stopThemeWatcher } from "../modes/interactive/theme/theme.js";
|
|
5
|
+
import { resolvePath } from "../utils/paths.js";
|
|
6
|
+
import { selectSession } from "./session-picker.js";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a session argument to a file path.
|
|
9
|
+
* If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
|
|
10
|
+
*/
|
|
11
|
+
async function findLocalSessionByExactId(sessionId, cwd, sessionDir) {
|
|
12
|
+
const localSessions = await SessionManager.list(cwd, sessionDir);
|
|
13
|
+
const localMatch = localSessions.find((s) => s.id === sessionId);
|
|
14
|
+
return localMatch ? { type: "local", path: localMatch.path } : undefined;
|
|
15
|
+
}
|
|
16
|
+
async function resolveSessionPath(sessionArg, cwd, sessionDir) {
|
|
17
|
+
// If it looks like a file path, resolve it before handing it to the session manager.
|
|
18
|
+
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
19
|
+
return { type: "path", path: resolvePath(sessionArg, cwd) };
|
|
20
|
+
}
|
|
21
|
+
// Try to match as session ID in current project first
|
|
22
|
+
const localSessions = await SessionManager.list(cwd, sessionDir);
|
|
23
|
+
const localMatch = localSessions.find((s) => s.id === sessionArg) ?? localSessions.find((s) => s.id.startsWith(sessionArg));
|
|
24
|
+
if (localMatch) {
|
|
25
|
+
return { type: "local", path: localMatch.path };
|
|
26
|
+
}
|
|
27
|
+
// Try global search across all projects
|
|
28
|
+
const allSessions = await SessionManager.listAll(sessionDir);
|
|
29
|
+
const globalMatch = allSessions.find((s) => s.id === sessionArg) ?? allSessions.find((s) => s.id.startsWith(sessionArg));
|
|
30
|
+
if (globalMatch) {
|
|
31
|
+
return { type: "global", path: globalMatch.path, cwd: globalMatch.cwd };
|
|
32
|
+
}
|
|
33
|
+
// Not found anywhere
|
|
34
|
+
return { type: "not_found", arg: sessionArg };
|
|
35
|
+
}
|
|
36
|
+
/** Prompt user for yes/no confirmation */
|
|
37
|
+
async function promptConfirm(message) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const rl = createInterface({
|
|
40
|
+
input: process.stdin,
|
|
41
|
+
output: process.stdout,
|
|
42
|
+
});
|
|
43
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
44
|
+
rl.close();
|
|
45
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function validateForkFlags(parsed) {
|
|
50
|
+
if (!parsed.fork)
|
|
51
|
+
return;
|
|
52
|
+
const conflictingFlags = [
|
|
53
|
+
parsed.session ? "--session" : undefined,
|
|
54
|
+
parsed.continue ? "--continue" : undefined,
|
|
55
|
+
parsed.resume ? "--resume" : undefined,
|
|
56
|
+
parsed.noSession ? "--no-session" : undefined,
|
|
57
|
+
].filter((flag) => flag !== undefined);
|
|
58
|
+
if (conflictingFlags.length > 0) {
|
|
59
|
+
console.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(", ")}`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function validateSessionIdFlags(parsed) {
|
|
64
|
+
if (parsed.sessionId === undefined)
|
|
65
|
+
return;
|
|
66
|
+
const conflictingFlags = [
|
|
67
|
+
parsed.session ? "--session" : undefined,
|
|
68
|
+
parsed.continue ? "--continue" : undefined,
|
|
69
|
+
parsed.resume ? "--resume" : undefined,
|
|
70
|
+
].filter((flag) => flag !== undefined);
|
|
71
|
+
if (conflictingFlags.length > 0) {
|
|
72
|
+
console.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(", ")}`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
assertValidSessionId(parsed.sessionId);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function openSessionOrExit(path, sessionDir) {
|
|
85
|
+
try {
|
|
86
|
+
return SessionManager.open(path, sessionDir);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function forkSessionOrExit(sourcePath, cwd, sessionDir, sessionId) {
|
|
95
|
+
try {
|
|
96
|
+
return SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
console.error(chalk.red(`Error: ${message}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
|
|
105
|
+
if (parsed.noSession || parsed.help || parsed.listModels !== undefined) {
|
|
106
|
+
return SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);
|
|
107
|
+
}
|
|
108
|
+
if (parsed.fork) {
|
|
109
|
+
if (parsed.sessionId) {
|
|
110
|
+
const existingTarget = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
|
|
111
|
+
if (existingTarget) {
|
|
112
|
+
console.error(chalk.red(`Session already exists with id '${parsed.sessionId}'`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);
|
|
117
|
+
switch (resolved.type) {
|
|
118
|
+
case "path":
|
|
119
|
+
case "local":
|
|
120
|
+
case "global":
|
|
121
|
+
return forkSessionOrExit(resolved.path, cwd, sessionDir, parsed.sessionId);
|
|
122
|
+
case "not_found":
|
|
123
|
+
console.error(chalk.red(`No session found matching '${resolved.arg}'`));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (parsed.session) {
|
|
128
|
+
const resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);
|
|
129
|
+
switch (resolved.type) {
|
|
130
|
+
case "path":
|
|
131
|
+
case "local":
|
|
132
|
+
return openSessionOrExit(resolved.path, sessionDir);
|
|
133
|
+
case "global": {
|
|
134
|
+
console.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));
|
|
135
|
+
const shouldFork = await promptConfirm("Fork this session into current directory?");
|
|
136
|
+
if (!shouldFork) {
|
|
137
|
+
console.log(chalk.dim("Aborted."));
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
return forkSessionOrExit(resolved.path, cwd, sessionDir);
|
|
141
|
+
}
|
|
142
|
+
case "not_found":
|
|
143
|
+
console.error(chalk.red(`No session found matching '${resolved.arg}'`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (parsed.resume) {
|
|
148
|
+
try {
|
|
149
|
+
const selectedPath = await selectSession((onProgress) => SessionManager.list(cwd, sessionDir, onProgress), (onProgress) => SessionManager.listAll(sessionDir, onProgress), settingsManager);
|
|
150
|
+
if (!selectedPath) {
|
|
151
|
+
console.log(chalk.dim("No session selected"));
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
return SessionManager.open(selectedPath, sessionDir);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
stopThemeWatcher();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (parsed.continue) {
|
|
161
|
+
return SessionManager.continueRecent(cwd, sessionDir);
|
|
162
|
+
}
|
|
163
|
+
if (parsed.sessionId) {
|
|
164
|
+
const existingSession = await findLocalSessionByExactId(parsed.sessionId, cwd, sessionDir);
|
|
165
|
+
if (existingSession) {
|
|
166
|
+
return SessionManager.open(existingSession.path, sessionDir);
|
|
167
|
+
}
|
|
168
|
+
console.error(chalk.yellow(`Warning: No project session found with id '${parsed.sessionId}'; creating a new session with that id.`));
|
|
169
|
+
}
|
|
170
|
+
return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });
|
|
171
|
+
}
|
|
172
|
+
export function applySessionName(parsed, sessionManager) {
|
|
173
|
+
if (parsed.name !== undefined) {
|
|
174
|
+
const name = parsed.name.trim();
|
|
175
|
+
if (!name) {
|
|
176
|
+
console.error(chalk.red("Error: --name requires a non-empty value"));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
sessionManager.appendSessionInfo(name);
|
|
180
|
+
}
|
|
181
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export declare const APP_NAME: string;
|
|
|
67
67
|
export declare const APP_TITLE: string;
|
|
68
68
|
export declare const CONFIG_DIR_NAME: string;
|
|
69
69
|
export declare const VERSION: string;
|
|
70
|
+
/** Set by distribution staging, independently of the user's NODE_ENV. */
|
|
71
|
+
export declare const IS_PRODUCTION_DISTRIBUTION: boolean;
|
|
70
72
|
export declare const ENV_AGENT_DIR = "DIFFEX_CODING_AGENT_DIR";
|
|
71
73
|
export declare const ENV_SESSION_DIR = "DIFFEX_CODING_AGENT_SESSION_DIR";
|
|
72
74
|
export declare function expandTildePath(path: string): string;
|
package/dist/config.js
CHANGED
|
@@ -393,6 +393,8 @@ export const APP_NAME = diffexConfigName || "diffex";
|
|
|
393
393
|
export const APP_TITLE = diffexConfigName ? APP_NAME : "Diffex";
|
|
394
394
|
export const CONFIG_DIR_NAME = pkg.diffexConfig?.configDir || ".diffex";
|
|
395
395
|
export const VERSION = pkg.version || "0.0.0";
|
|
396
|
+
/** Set by distribution staging, independently of the user's NODE_ENV. */
|
|
397
|
+
export const IS_PRODUCTION_DISTRIBUTION = isBunBinary || pkg.diffexConfig?.production === true;
|
|
396
398
|
export const ENV_AGENT_DIR = "DIFFEX_CODING_AGENT_DIR";
|
|
397
399
|
export const ENV_SESSION_DIR = "DIFFEX_CODING_AGENT_SESSION_DIR";
|
|
398
400
|
export function expandTildePath(path) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EventBus } from "../event-bus.ts";
|
|
2
|
+
import type { Extension, ExtensionAPI, ExtensionFactory, ExtensionRuntime } from "./types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Create a runtime with throwing stubs for action methods.
|
|
5
|
+
* Runner.bindCore() replaces these with real implementations.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createExtensionRuntime(): ExtensionRuntime;
|
|
8
|
+
/**
|
|
9
|
+
* Create the ExtensionAPI for an extension.
|
|
10
|
+
* Registration methods write to the extension object.
|
|
11
|
+
* Action methods delegate to the shared runtime.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createExtensionAPI(extension: Extension, runtime: ExtensionRuntime, cwd: string, eventBus: EventBus): ExtensionAPI;
|
|
14
|
+
/**
|
|
15
|
+
* Create an Extension object with empty collections.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createExtension(extensionPath: string, resolvedPath: string): Extension;
|
|
18
|
+
/**
|
|
19
|
+
* Create an Extension from an inline factory function.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, extensionPath?: string): Promise<Extension>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { resolvePath } from "../../utils/paths.js";
|
|
3
|
+
import { execCommand } from "../exec.js";
|
|
4
|
+
import { createSyntheticSourceInfo } from "../source-info.js";
|
|
5
|
+
import { time } from "../timings.js";
|
|
6
|
+
/**
|
|
7
|
+
* Create a runtime with throwing stubs for action methods.
|
|
8
|
+
* Runner.bindCore() replaces these with real implementations.
|
|
9
|
+
*/
|
|
10
|
+
export function createExtensionRuntime() {
|
|
11
|
+
const notInitialized = () => {
|
|
12
|
+
throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
|
|
13
|
+
};
|
|
14
|
+
const state = {};
|
|
15
|
+
const eventBusUnsubscribers = new Set();
|
|
16
|
+
const assertActive = () => {
|
|
17
|
+
if (state.staleMessage) {
|
|
18
|
+
throw new Error(state.staleMessage);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const runtime = {
|
|
22
|
+
sendMessage: notInitialized,
|
|
23
|
+
sendUserMessage: notInitialized,
|
|
24
|
+
appendEntry: notInitialized,
|
|
25
|
+
setSessionName: notInitialized,
|
|
26
|
+
getSessionName: notInitialized,
|
|
27
|
+
setLabel: notInitialized,
|
|
28
|
+
getActiveTools: notInitialized,
|
|
29
|
+
getAllTools: notInitialized,
|
|
30
|
+
setActiveTools: notInitialized,
|
|
31
|
+
// registerTool() is valid during extension load; refresh is only needed post-bind.
|
|
32
|
+
refreshTools: () => { },
|
|
33
|
+
getCommands: notInitialized,
|
|
34
|
+
setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
|
|
35
|
+
getThinkingLevel: notInitialized,
|
|
36
|
+
setThinkingLevel: notInitialized,
|
|
37
|
+
flagValues: new Map(),
|
|
38
|
+
pendingProviderRegistrations: [],
|
|
39
|
+
pendingNativeProviderRegistrations: [],
|
|
40
|
+
assertActive,
|
|
41
|
+
invalidate: (message) => {
|
|
42
|
+
if (state.staleMessage)
|
|
43
|
+
return;
|
|
44
|
+
state.staleMessage =
|
|
45
|
+
message ??
|
|
46
|
+
"This extension ctx is stale after session replacement or reload. Do not use a captured Diffex API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";
|
|
47
|
+
for (const unsubscribe of eventBusUnsubscribers)
|
|
48
|
+
unsubscribe();
|
|
49
|
+
eventBusUnsubscribers.clear();
|
|
50
|
+
},
|
|
51
|
+
trackEventBusSubscription: (unsubscribe) => {
|
|
52
|
+
let active = true;
|
|
53
|
+
const trackedUnsubscribe = () => {
|
|
54
|
+
if (!active)
|
|
55
|
+
return;
|
|
56
|
+
active = false;
|
|
57
|
+
eventBusUnsubscribers.delete(trackedUnsubscribe);
|
|
58
|
+
unsubscribe();
|
|
59
|
+
};
|
|
60
|
+
eventBusUnsubscribers.add(trackedUnsubscribe);
|
|
61
|
+
return trackedUnsubscribe;
|
|
62
|
+
},
|
|
63
|
+
// Pre-bind: queue registrations so bindCore() can flush them once the
|
|
64
|
+
// model registry is available. bindCore() replaces both with direct calls.
|
|
65
|
+
registerProvider: (name, config, extensionPath = "<unknown>") => {
|
|
66
|
+
runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
|
|
67
|
+
},
|
|
68
|
+
registerNativeProvider: (provider, extensionPath = "<unknown>") => {
|
|
69
|
+
runtime.pendingNativeProviderRegistrations.push({ provider, extensionPath });
|
|
70
|
+
},
|
|
71
|
+
unregisterProvider: (name) => {
|
|
72
|
+
runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
|
|
73
|
+
runtime.pendingNativeProviderRegistrations = runtime.pendingNativeProviderRegistrations.filter((r) => r.provider.id !== name);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
return runtime;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create the ExtensionAPI for an extension.
|
|
80
|
+
* Registration methods write to the extension object.
|
|
81
|
+
* Action methods delegate to the shared runtime.
|
|
82
|
+
*/
|
|
83
|
+
export function createExtensionAPI(extension, runtime, cwd, eventBus) {
|
|
84
|
+
const api = {
|
|
85
|
+
// Registration methods - write to extension
|
|
86
|
+
on(event, handler) {
|
|
87
|
+
runtime.assertActive();
|
|
88
|
+
const list = extension.handlers.get(event) ?? [];
|
|
89
|
+
list.push(handler);
|
|
90
|
+
extension.handlers.set(event, list);
|
|
91
|
+
},
|
|
92
|
+
registerTool(tool) {
|
|
93
|
+
runtime.assertActive();
|
|
94
|
+
extension.tools.set(tool.name, {
|
|
95
|
+
definition: tool,
|
|
96
|
+
sourceInfo: extension.sourceInfo,
|
|
97
|
+
});
|
|
98
|
+
runtime.refreshTools();
|
|
99
|
+
},
|
|
100
|
+
registerCommand(name, options) {
|
|
101
|
+
runtime.assertActive();
|
|
102
|
+
extension.commands.set(name, {
|
|
103
|
+
name,
|
|
104
|
+
sourceInfo: extension.sourceInfo,
|
|
105
|
+
...options,
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
registerShortcut(shortcut, options) {
|
|
109
|
+
runtime.assertActive();
|
|
110
|
+
extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
|
|
111
|
+
},
|
|
112
|
+
registerFlag(name, options) {
|
|
113
|
+
runtime.assertActive();
|
|
114
|
+
extension.flags.set(name, { name, extensionPath: extension.path, ...options });
|
|
115
|
+
if (options.default !== undefined && !runtime.flagValues.has(name)) {
|
|
116
|
+
runtime.flagValues.set(name, options.default);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
registerMessageRenderer(customType, renderer) {
|
|
120
|
+
runtime.assertActive();
|
|
121
|
+
extension.messageRenderers.set(customType, renderer);
|
|
122
|
+
},
|
|
123
|
+
registerMarkdownTransformer(transformer) {
|
|
124
|
+
runtime.assertActive();
|
|
125
|
+
extension.markdownTransformer = transformer;
|
|
126
|
+
},
|
|
127
|
+
registerEntryRenderer(customType, renderer) {
|
|
128
|
+
runtime.assertActive();
|
|
129
|
+
extension.entryRenderers ??= new Map();
|
|
130
|
+
extension.entryRenderers.set(customType, renderer);
|
|
131
|
+
},
|
|
132
|
+
// Flag access - checks extension registered it, reads from runtime
|
|
133
|
+
getFlag(name) {
|
|
134
|
+
runtime.assertActive();
|
|
135
|
+
if (!extension.flags.has(name))
|
|
136
|
+
return undefined;
|
|
137
|
+
return runtime.flagValues.get(name);
|
|
138
|
+
},
|
|
139
|
+
// Action methods - delegate to shared runtime
|
|
140
|
+
sendMessage(message, options) {
|
|
141
|
+
runtime.assertActive();
|
|
142
|
+
runtime.sendMessage(message, options);
|
|
143
|
+
},
|
|
144
|
+
sendUserMessage(content, options) {
|
|
145
|
+
runtime.assertActive();
|
|
146
|
+
runtime.sendUserMessage(content, options);
|
|
147
|
+
},
|
|
148
|
+
appendEntry(customType, data) {
|
|
149
|
+
runtime.assertActive();
|
|
150
|
+
runtime.appendEntry(customType, data);
|
|
151
|
+
},
|
|
152
|
+
setSessionName(name) {
|
|
153
|
+
runtime.assertActive();
|
|
154
|
+
runtime.setSessionName(name);
|
|
155
|
+
},
|
|
156
|
+
getSessionName() {
|
|
157
|
+
runtime.assertActive();
|
|
158
|
+
return runtime.getSessionName();
|
|
159
|
+
},
|
|
160
|
+
setLabel(entryId, label) {
|
|
161
|
+
runtime.assertActive();
|
|
162
|
+
runtime.setLabel(entryId, label);
|
|
163
|
+
},
|
|
164
|
+
exec(command, args, options) {
|
|
165
|
+
runtime.assertActive();
|
|
166
|
+
return execCommand(command, args, options?.cwd ?? cwd, options);
|
|
167
|
+
},
|
|
168
|
+
getActiveTools() {
|
|
169
|
+
runtime.assertActive();
|
|
170
|
+
return runtime.getActiveTools();
|
|
171
|
+
},
|
|
172
|
+
getAllTools() {
|
|
173
|
+
runtime.assertActive();
|
|
174
|
+
return runtime.getAllTools();
|
|
175
|
+
},
|
|
176
|
+
setActiveTools(toolNames) {
|
|
177
|
+
runtime.assertActive();
|
|
178
|
+
runtime.setActiveTools(toolNames);
|
|
179
|
+
},
|
|
180
|
+
getCommands() {
|
|
181
|
+
runtime.assertActive();
|
|
182
|
+
return runtime.getCommands();
|
|
183
|
+
},
|
|
184
|
+
setModel(model) {
|
|
185
|
+
runtime.assertActive();
|
|
186
|
+
return runtime.setModel(model);
|
|
187
|
+
},
|
|
188
|
+
getThinkingLevel() {
|
|
189
|
+
runtime.assertActive();
|
|
190
|
+
return runtime.getThinkingLevel();
|
|
191
|
+
},
|
|
192
|
+
setThinkingLevel(level) {
|
|
193
|
+
runtime.assertActive();
|
|
194
|
+
runtime.setThinkingLevel(level);
|
|
195
|
+
},
|
|
196
|
+
registerProvider(providerOrName, config) {
|
|
197
|
+
runtime.assertActive();
|
|
198
|
+
if (typeof providerOrName === "string") {
|
|
199
|
+
if (!config)
|
|
200
|
+
throw new Error("Provider config is required when registering by name");
|
|
201
|
+
runtime.registerProvider(providerOrName, config, extension.path);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
runtime.registerNativeProvider(providerOrName, extension.path);
|
|
205
|
+
},
|
|
206
|
+
unregisterProvider(name) {
|
|
207
|
+
runtime.assertActive();
|
|
208
|
+
runtime.unregisterProvider(name, extension.path);
|
|
209
|
+
},
|
|
210
|
+
events: {
|
|
211
|
+
emit(channel, data) {
|
|
212
|
+
runtime.assertActive();
|
|
213
|
+
eventBus.emit(channel, data);
|
|
214
|
+
},
|
|
215
|
+
on(channel, handler) {
|
|
216
|
+
runtime.assertActive();
|
|
217
|
+
return runtime.trackEventBusSubscription(eventBus.on(channel, handler));
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
return api;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create an Extension object with empty collections.
|
|
225
|
+
*/
|
|
226
|
+
export function createExtension(extensionPath, resolvedPath) {
|
|
227
|
+
const source = extensionPath.startsWith("<") && extensionPath.endsWith(">")
|
|
228
|
+
? extensionPath.slice(1, -1).split(":")[0] || "temporary"
|
|
229
|
+
: "local";
|
|
230
|
+
const baseDir = extensionPath.startsWith("<") ? undefined : path.dirname(resolvedPath);
|
|
231
|
+
return {
|
|
232
|
+
path: extensionPath,
|
|
233
|
+
resolvedPath,
|
|
234
|
+
sourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),
|
|
235
|
+
handlers: new Map(),
|
|
236
|
+
tools: new Map(),
|
|
237
|
+
messageRenderers: new Map(),
|
|
238
|
+
entryRenderers: new Map(),
|
|
239
|
+
commands: new Map(),
|
|
240
|
+
flags: new Map(),
|
|
241
|
+
shortcuts: new Map(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Create an Extension from an inline factory function.
|
|
246
|
+
*/
|
|
247
|
+
export async function loadExtensionFromFactory(factory, cwd, eventBus, runtime, extensionPath = "<inline>") {
|
|
248
|
+
const extension = createExtension(extensionPath, extensionPath);
|
|
249
|
+
const resolvedCwd = resolvePath(cwd);
|
|
250
|
+
const api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);
|
|
251
|
+
await factory(api);
|
|
252
|
+
time(`${extensionPath} factory`, "extensions");
|
|
253
|
+
return extension;
|
|
254
|
+
}
|
|
@@ -3,17 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
*/
|
|
5
5
|
import { type EventBus } from "../event-bus.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ExtensionRuntime, LoadExtensionsResult } from "./types.ts";
|
|
7
|
+
export { createExtensionRuntime, loadExtensionFromFactory } from "./factory.ts";
|
|
7
8
|
export declare function clearExtensionCache(): void;
|
|
8
|
-
/**
|
|
9
|
-
* Create a runtime with throwing stubs for action methods.
|
|
10
|
-
* Runner.bindCore() replaces these with real implementations.
|
|
11
|
-
*/
|
|
12
|
-
export declare function createExtensionRuntime(): ExtensionRuntime;
|
|
13
|
-
/**
|
|
14
|
-
* Create an Extension from an inline factory function.
|
|
15
|
-
*/
|
|
16
|
-
export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, extensionPath?: string): Promise<Extension>;
|
|
17
9
|
export declare function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus, runtime?: ExtensionRuntime): Promise<LoadExtensionsResult>;
|
|
18
10
|
export declare function loadExtensionsCached(paths: string[], cwd: string, eventBus?: EventBus, runtime?: ExtensionRuntime): Promise<LoadExtensionsResult>;
|
|
19
11
|
/**
|