@clinebot/core 0.0.0
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 +88 -0
- package/dist/account/cline-account-service.d.ts +34 -0
- package/dist/account/index.d.ts +3 -0
- package/dist/account/rpc.d.ts +38 -0
- package/dist/account/types.d.ts +74 -0
- package/dist/agents/agent-config-loader.d.ts +18 -0
- package/dist/agents/agent-config-parser.d.ts +25 -0
- package/dist/agents/hooks-config-loader.d.ts +23 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/plugin-config-loader.d.ts +22 -0
- package/dist/agents/plugin-loader.d.ts +9 -0
- package/dist/agents/plugin-sandbox.d.ts +12 -0
- package/dist/agents/unified-config-file-watcher.d.ts +77 -0
- package/dist/agents/user-instruction-config-loader.d.ts +63 -0
- package/dist/auth/client.d.ts +11 -0
- package/dist/auth/cline.d.ts +41 -0
- package/dist/auth/codex.d.ts +39 -0
- package/dist/auth/oca.d.ts +22 -0
- package/dist/auth/server.d.ts +22 -0
- package/dist/auth/types.d.ts +72 -0
- package/dist/auth/utils.d.ts +32 -0
- package/dist/chat/chat-schema.d.ts +145 -0
- package/dist/default-tools/constants.d.ts +23 -0
- package/dist/default-tools/definitions.d.ts +96 -0
- package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
- package/dist/default-tools/executors/apply-patch.d.ts +26 -0
- package/dist/default-tools/executors/bash.d.ts +49 -0
- package/dist/default-tools/executors/editor.d.ts +31 -0
- package/dist/default-tools/executors/file-read.d.ts +40 -0
- package/dist/default-tools/executors/index.d.ts +44 -0
- package/dist/default-tools/executors/search.d.ts +50 -0
- package/dist/default-tools/executors/web-fetch.d.ts +58 -0
- package/dist/default-tools/index.d.ts +57 -0
- package/dist/default-tools/presets.d.ts +124 -0
- package/dist/default-tools/schemas.d.ts +121 -0
- package/dist/default-tools/types.d.ts +237 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +220 -0
- package/dist/input/file-indexer.d.ts +5 -0
- package/dist/input/index.d.ts +4 -0
- package/dist/input/mention-enricher.d.ts +12 -0
- package/dist/mcp/config-loader.d.ts +15 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/types.d.ts +66 -0
- package/dist/runtime/hook-file-hooks.d.ts +18 -0
- package/dist/runtime/rules.d.ts +5 -0
- package/dist/runtime/runtime-builder.d.ts +5 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
- package/dist/runtime/session-runtime.d.ts +36 -0
- package/dist/runtime/tool-approval.d.ts +9 -0
- package/dist/runtime/workflows.d.ts +13 -0
- package/dist/server/index.d.ts +47 -0
- package/dist/server/index.js +641 -0
- package/dist/session/default-session-manager.d.ts +77 -0
- package/dist/session/rpc-session-service.d.ts +12 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
- package/dist/session/session-artifacts.d.ts +19 -0
- package/dist/session/session-graph.d.ts +15 -0
- package/dist/session/session-host.d.ts +21 -0
- package/dist/session/session-manager.d.ts +50 -0
- package/dist/session/session-manifest.d.ts +30 -0
- package/dist/session/session-service.d.ts +113 -0
- package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
- package/dist/session/unified-session-persistence-service.d.ts +93 -0
- package/dist/session/workspace-manager.d.ts +28 -0
- package/dist/session/workspace-manifest.d.ts +25 -0
- package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
- package/dist/storage/provider-settings-manager.d.ts +20 -0
- package/dist/storage/sqlite-session-store.d.ts +29 -0
- package/dist/storage/sqlite-team-store.d.ts +31 -0
- package/dist/storage/team-store.d.ts +2 -0
- package/dist/team/index.d.ts +1 -0
- package/dist/team/projections.d.ts +8 -0
- package/dist/types/common.d.ts +10 -0
- package/dist/types/config.d.ts +37 -0
- package/dist/types/events.d.ts +54 -0
- package/dist/types/provider-settings.d.ts +20 -0
- package/dist/types/sessions.d.ts +9 -0
- package/dist/types/storage.d.ts +37 -0
- package/dist/types/workspace.d.ts +7 -0
- package/dist/types.d.ts +26 -0
- package/package.json +63 -0
- package/src/account/cline-account-service.test.ts +101 -0
- package/src/account/cline-account-service.ts +267 -0
- package/src/account/index.ts +20 -0
- package/src/account/rpc.test.ts +62 -0
- package/src/account/rpc.ts +172 -0
- package/src/account/types.ts +80 -0
- package/src/agents/agent-config-loader.test.ts +234 -0
- package/src/agents/agent-config-loader.ts +107 -0
- package/src/agents/agent-config-parser.ts +191 -0
- package/src/agents/hooks-config-loader.ts +97 -0
- package/src/agents/index.ts +84 -0
- package/src/agents/plugin-config-loader.test.ts +91 -0
- package/src/agents/plugin-config-loader.ts +160 -0
- package/src/agents/plugin-loader.test.ts +102 -0
- package/src/agents/plugin-loader.ts +105 -0
- package/src/agents/plugin-sandbox.test.ts +120 -0
- package/src/agents/plugin-sandbox.ts +471 -0
- package/src/agents/unified-config-file-watcher.test.ts +196 -0
- package/src/agents/unified-config-file-watcher.ts +483 -0
- package/src/agents/user-instruction-config-loader.test.ts +158 -0
- package/src/agents/user-instruction-config-loader.ts +438 -0
- package/src/auth/client.test.ts +40 -0
- package/src/auth/client.ts +25 -0
- package/src/auth/cline.test.ts +130 -0
- package/src/auth/cline.ts +414 -0
- package/src/auth/codex.test.ts +170 -0
- package/src/auth/codex.ts +466 -0
- package/src/auth/oca.test.ts +215 -0
- package/src/auth/oca.ts +546 -0
- package/src/auth/server.ts +216 -0
- package/src/auth/types.ts +78 -0
- package/src/auth/utils.test.ts +128 -0
- package/src/auth/utils.ts +247 -0
- package/src/chat/chat-schema.ts +82 -0
- package/src/default-tools/constants.ts +35 -0
- package/src/default-tools/definitions.test.ts +233 -0
- package/src/default-tools/definitions.ts +632 -0
- package/src/default-tools/executors/apply-patch-parser.ts +520 -0
- package/src/default-tools/executors/apply-patch.ts +359 -0
- package/src/default-tools/executors/bash.ts +205 -0
- package/src/default-tools/executors/editor.ts +231 -0
- package/src/default-tools/executors/file-read.test.ts +25 -0
- package/src/default-tools/executors/file-read.ts +94 -0
- package/src/default-tools/executors/index.ts +75 -0
- package/src/default-tools/executors/search.ts +278 -0
- package/src/default-tools/executors/web-fetch.ts +259 -0
- package/src/default-tools/index.ts +161 -0
- package/src/default-tools/presets.test.ts +63 -0
- package/src/default-tools/presets.ts +168 -0
- package/src/default-tools/schemas.ts +228 -0
- package/src/default-tools/types.ts +324 -0
- package/src/index.ts +119 -0
- package/src/input/file-indexer.d.ts +11 -0
- package/src/input/file-indexer.test.ts +87 -0
- package/src/input/file-indexer.ts +280 -0
- package/src/input/index.ts +7 -0
- package/src/input/mention-enricher.test.ts +82 -0
- package/src/input/mention-enricher.ts +119 -0
- package/src/mcp/config-loader.test.ts +238 -0
- package/src/mcp/config-loader.ts +219 -0
- package/src/mcp/index.ts +26 -0
- package/src/mcp/manager.test.ts +106 -0
- package/src/mcp/manager.ts +262 -0
- package/src/mcp/types.ts +88 -0
- package/src/runtime/hook-file-hooks.test.ts +106 -0
- package/src/runtime/hook-file-hooks.ts +736 -0
- package/src/runtime/index.ts +27 -0
- package/src/runtime/rules.ts +34 -0
- package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
- package/src/runtime/runtime-builder.test.ts +215 -0
- package/src/runtime/runtime-builder.ts +515 -0
- package/src/runtime/runtime-parity.test.ts +132 -0
- package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
- package/src/runtime/session-runtime.ts +44 -0
- package/src/runtime/tool-approval.ts +104 -0
- package/src/runtime/workflows.test.ts +119 -0
- package/src/runtime/workflows.ts +54 -0
- package/src/server/index.ts +282 -0
- package/src/session/default-session-manager.e2e.test.ts +354 -0
- package/src/session/default-session-manager.test.ts +816 -0
- package/src/session/default-session-manager.ts +1286 -0
- package/src/session/index.ts +37 -0
- package/src/session/rpc-session-service.ts +189 -0
- package/src/session/runtime-oauth-token-manager.test.ts +137 -0
- package/src/session/runtime-oauth-token-manager.ts +265 -0
- package/src/session/session-artifacts.ts +106 -0
- package/src/session/session-graph.ts +90 -0
- package/src/session/session-host.ts +190 -0
- package/src/session/session-manager.ts +56 -0
- package/src/session/session-manifest.ts +29 -0
- package/src/session/session-service.team-persistence.test.ts +48 -0
- package/src/session/session-service.ts +610 -0
- package/src/session/sqlite-rpc-session-backend.ts +303 -0
- package/src/session/unified-session-persistence-service.ts +781 -0
- package/src/session/workspace-manager.ts +98 -0
- package/src/session/workspace-manifest.ts +100 -0
- package/src/storage/artifact-store.ts +1 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
- package/src/storage/provider-settings-legacy-migration.ts +637 -0
- package/src/storage/provider-settings-manager.test.ts +111 -0
- package/src/storage/provider-settings-manager.ts +129 -0
- package/src/storage/session-store.ts +1 -0
- package/src/storage/sqlite-session-store.ts +270 -0
- package/src/storage/sqlite-team-store.ts +443 -0
- package/src/storage/team-store.ts +5 -0
- package/src/team/index.ts +4 -0
- package/src/team/projections.ts +285 -0
- package/src/types/common.ts +14 -0
- package/src/types/config.ts +64 -0
- package/src/types/events.ts +46 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider-settings.ts +43 -0
- package/src/types/sessions.ts +16 -0
- package/src/types/storage.ts +64 -0
- package/src/types/workspace.ts +7 -0
- package/src/types.ts +127 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
RULES_CONFIG_DIRECTORY_NAME,
|
|
5
|
+
resolveDocumentsRulesDirectoryPath,
|
|
6
|
+
resolveDocumentsWorkflowsDirectoryPath,
|
|
7
|
+
resolveRulesConfigSearchPaths as resolveRulesConfigSearchPathsFromShared,
|
|
8
|
+
resolveSkillsConfigSearchPaths as resolveSkillsConfigSearchPathsFromShared,
|
|
9
|
+
resolveWorkflowsConfigSearchPaths as resolveWorkflowsConfigSearchPathsFromShared,
|
|
10
|
+
SKILLS_CONFIG_DIRECTORY_NAME,
|
|
11
|
+
WORKFLOWS_CONFIG_DIRECTORY_NAME,
|
|
12
|
+
} from "@clinebot/shared/storage";
|
|
13
|
+
import YAML from "yaml";
|
|
14
|
+
import {
|
|
15
|
+
type UnifiedConfigDefinition,
|
|
16
|
+
type UnifiedConfigFileCandidate,
|
|
17
|
+
UnifiedConfigFileWatcher,
|
|
18
|
+
type UnifiedConfigWatcherEvent,
|
|
19
|
+
} from "./unified-config-file-watcher";
|
|
20
|
+
|
|
21
|
+
const SKILL_FILE_NAME = "SKILL.md";
|
|
22
|
+
|
|
23
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
RULES_CONFIG_DIRECTORY_NAME,
|
|
27
|
+
resolveDocumentsRulesDirectoryPath,
|
|
28
|
+
resolveDocumentsWorkflowsDirectoryPath,
|
|
29
|
+
SKILLS_CONFIG_DIRECTORY_NAME,
|
|
30
|
+
WORKFLOWS_CONFIG_DIRECTORY_NAME,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface ParseMarkdownFrontmatterResult {
|
|
34
|
+
data: Record<string, unknown>;
|
|
35
|
+
body: string;
|
|
36
|
+
hadFrontmatter: boolean;
|
|
37
|
+
parseError?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkillConfig {
|
|
41
|
+
name: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
instructions: string;
|
|
45
|
+
frontmatter: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface RuleConfig {
|
|
49
|
+
name: string;
|
|
50
|
+
disabled?: boolean;
|
|
51
|
+
instructions: string;
|
|
52
|
+
frontmatter: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkflowConfig {
|
|
56
|
+
name: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
instructions: string;
|
|
59
|
+
frontmatter: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type UserInstructionConfigType = "skill" | "rule" | "workflow";
|
|
63
|
+
|
|
64
|
+
export type UserInstructionConfig = SkillConfig | RuleConfig | WorkflowConfig;
|
|
65
|
+
|
|
66
|
+
export type UserInstructionConfigWatcher = UnifiedConfigFileWatcher<
|
|
67
|
+
UserInstructionConfigType,
|
|
68
|
+
UserInstructionConfig
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
export type UserInstructionConfigWatcherEvent = UnifiedConfigWatcherEvent<
|
|
72
|
+
UserInstructionConfigType,
|
|
73
|
+
UserInstructionConfig
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
export interface CreateInstructionWatcherOptions {
|
|
77
|
+
debounceMs?: number;
|
|
78
|
+
emitParseErrors?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CreateSkillsConfigDefinitionOptions {
|
|
82
|
+
directories?: ReadonlyArray<string>;
|
|
83
|
+
workspacePath?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CreateRulesConfigDefinitionOptions {
|
|
87
|
+
directories?: ReadonlyArray<string>;
|
|
88
|
+
workspacePath?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CreateWorkflowsConfigDefinitionOptions {
|
|
92
|
+
directories?: ReadonlyArray<string>;
|
|
93
|
+
workspacePath?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeName(name: string): string {
|
|
97
|
+
return name.trim().toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isMarkdownFile(fileName: string): boolean {
|
|
101
|
+
return MARKDOWN_EXTENSIONS.has(extname(fileName).toLowerCase());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseMarkdownFrontmatter(
|
|
105
|
+
content: string,
|
|
106
|
+
): ParseMarkdownFrontmatterResult {
|
|
107
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
108
|
+
const match = content.match(frontmatterRegex);
|
|
109
|
+
if (!match) {
|
|
110
|
+
return { data: {}, body: content, hadFrontmatter: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [, yamlContent, body] = match;
|
|
114
|
+
try {
|
|
115
|
+
const parsed = YAML.parse(yamlContent);
|
|
116
|
+
const data =
|
|
117
|
+
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
118
|
+
? (parsed as Record<string, unknown>)
|
|
119
|
+
: {};
|
|
120
|
+
return { data, body, hadFrontmatter: true };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
return {
|
|
124
|
+
data: {},
|
|
125
|
+
body: content,
|
|
126
|
+
hadFrontmatter: true,
|
|
127
|
+
parseError: message,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseStringField(
|
|
133
|
+
value: unknown,
|
|
134
|
+
fieldName: string,
|
|
135
|
+
isRequired: boolean,
|
|
136
|
+
): string | undefined {
|
|
137
|
+
if (value === undefined || value === null) {
|
|
138
|
+
if (isRequired) {
|
|
139
|
+
throw new Error(`Missing required frontmatter field '${fieldName}'.`);
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (typeof value !== "string") {
|
|
144
|
+
throw new Error(`Frontmatter field '${fieldName}' must be a string.`);
|
|
145
|
+
}
|
|
146
|
+
const normalized = value.trim();
|
|
147
|
+
if (!normalized && isRequired) {
|
|
148
|
+
throw new Error(`Frontmatter field '${fieldName}' cannot be empty.`);
|
|
149
|
+
}
|
|
150
|
+
return normalized || undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseBooleanField(
|
|
154
|
+
value: unknown,
|
|
155
|
+
fieldName: string,
|
|
156
|
+
): boolean | undefined {
|
|
157
|
+
if (value === undefined || value === null) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
if (typeof value !== "boolean") {
|
|
161
|
+
throw new Error(`Frontmatter field '${fieldName}' must be a boolean.`);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function parseSkillConfigFromMarkdown(
|
|
167
|
+
content: string,
|
|
168
|
+
fallbackName: string,
|
|
169
|
+
): SkillConfig {
|
|
170
|
+
const { data, body, parseError } = parseMarkdownFrontmatter(content);
|
|
171
|
+
if (parseError) {
|
|
172
|
+
throw new Error(`Failed to parse YAML frontmatter: ${parseError}`);
|
|
173
|
+
}
|
|
174
|
+
const instructions = body.trim();
|
|
175
|
+
if (!instructions) {
|
|
176
|
+
throw new Error("Missing instructions body in skill file.");
|
|
177
|
+
}
|
|
178
|
+
const parsedName = parseStringField(data.name, "name", false);
|
|
179
|
+
const name = parsedName ?? fallbackName.trim();
|
|
180
|
+
if (!name) {
|
|
181
|
+
throw new Error("Missing skill name.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
name,
|
|
186
|
+
description: parseStringField(data.description, "description", false),
|
|
187
|
+
disabled:
|
|
188
|
+
parseBooleanField(data.disabled, "disabled") ??
|
|
189
|
+
(parseBooleanField(data.enabled, "enabled") === false ? true : undefined),
|
|
190
|
+
instructions,
|
|
191
|
+
frontmatter: data,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function parseRuleConfigFromMarkdown(
|
|
196
|
+
content: string,
|
|
197
|
+
fallbackName: string,
|
|
198
|
+
): RuleConfig {
|
|
199
|
+
const { data, body, parseError } = parseMarkdownFrontmatter(content);
|
|
200
|
+
if (parseError) {
|
|
201
|
+
throw new Error(`Failed to parse YAML frontmatter: ${parseError}`);
|
|
202
|
+
}
|
|
203
|
+
const instructions = body.trim();
|
|
204
|
+
if (!instructions) {
|
|
205
|
+
throw new Error("Missing instructions body in rule file.");
|
|
206
|
+
}
|
|
207
|
+
const name =
|
|
208
|
+
parseStringField(data.name, "name", false) ?? fallbackName.trim();
|
|
209
|
+
if (!name) {
|
|
210
|
+
throw new Error("Missing rule name.");
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
name,
|
|
214
|
+
disabled:
|
|
215
|
+
parseBooleanField(data.disabled, "disabled") ??
|
|
216
|
+
(parseBooleanField(data.enabled, "enabled") === false ? true : undefined),
|
|
217
|
+
instructions,
|
|
218
|
+
frontmatter: data,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function parseWorkflowConfigFromMarkdown(
|
|
223
|
+
content: string,
|
|
224
|
+
fallbackName: string,
|
|
225
|
+
): WorkflowConfig {
|
|
226
|
+
const { data, body, parseError } = parseMarkdownFrontmatter(content);
|
|
227
|
+
if (parseError) {
|
|
228
|
+
throw new Error(`Failed to parse YAML frontmatter: ${parseError}`);
|
|
229
|
+
}
|
|
230
|
+
const instructions = body.trim();
|
|
231
|
+
if (!instructions) {
|
|
232
|
+
throw new Error("Missing instructions body in workflow file.");
|
|
233
|
+
}
|
|
234
|
+
const name =
|
|
235
|
+
parseStringField(data.name, "name", false) ?? fallbackName.trim();
|
|
236
|
+
if (!name) {
|
|
237
|
+
throw new Error("Missing workflow name.");
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
name,
|
|
241
|
+
disabled:
|
|
242
|
+
parseBooleanField(data.disabled, "disabled") ??
|
|
243
|
+
(parseBooleanField(data.enabled, "enabled") === false ? true : undefined),
|
|
244
|
+
instructions,
|
|
245
|
+
frontmatter: data,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolveSkillsConfigSearchPaths(
|
|
250
|
+
workspacePath?: string,
|
|
251
|
+
): string[] {
|
|
252
|
+
return resolveSkillsConfigSearchPathsFromShared(workspacePath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function resolveRulesConfigSearchPaths(
|
|
256
|
+
workspacePath?: string,
|
|
257
|
+
): string[] {
|
|
258
|
+
return resolveRulesConfigSearchPathsFromShared(workspacePath);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function resolveWorkflowsConfigSearchPaths(
|
|
262
|
+
workspacePath?: string,
|
|
263
|
+
): string[] {
|
|
264
|
+
return resolveWorkflowsConfigSearchPathsFromShared(workspacePath);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function discoverSkillFiles(
|
|
268
|
+
directoryPath: string,
|
|
269
|
+
): Promise<ReadonlyArray<UnifiedConfigFileCandidate>> {
|
|
270
|
+
try {
|
|
271
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
272
|
+
const candidates: UnifiedConfigFileCandidate[] = [];
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (entry.isFile() && entry.name === SKILL_FILE_NAME) {
|
|
275
|
+
candidates.push({
|
|
276
|
+
directoryPath,
|
|
277
|
+
fileName: entry.name,
|
|
278
|
+
filePath: join(directoryPath, entry.name),
|
|
279
|
+
});
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (entry.isDirectory()) {
|
|
283
|
+
candidates.push({
|
|
284
|
+
directoryPath: join(directoryPath, entry.name),
|
|
285
|
+
fileName: SKILL_FILE_NAME,
|
|
286
|
+
filePath: join(directoryPath, entry.name, SKILL_FILE_NAME),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return candidates;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
293
|
+
if (nodeError.code === "ENOENT") {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function discoverRulesLikeFiles(
|
|
301
|
+
directoryPath: string,
|
|
302
|
+
): Promise<ReadonlyArray<UnifiedConfigFileCandidate>> {
|
|
303
|
+
try {
|
|
304
|
+
const entryStat = await stat(directoryPath);
|
|
305
|
+
if (entryStat.isFile()) {
|
|
306
|
+
return [
|
|
307
|
+
{
|
|
308
|
+
directoryPath: dirname(directoryPath),
|
|
309
|
+
fileName: basename(directoryPath),
|
|
310
|
+
filePath: directoryPath,
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
316
|
+
if (nodeError.code !== "ENOENT") {
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const entries = await readdir(directoryPath, { withFileTypes: true });
|
|
323
|
+
return entries
|
|
324
|
+
.filter((entry) => entry.isFile() && isMarkdownFile(entry.name))
|
|
325
|
+
.map((entry) => ({
|
|
326
|
+
directoryPath,
|
|
327
|
+
fileName: entry.name,
|
|
328
|
+
filePath: join(directoryPath, entry.name),
|
|
329
|
+
}));
|
|
330
|
+
} catch (error) {
|
|
331
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
332
|
+
if (nodeError.code === "ENOENT") {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function createSkillsConfigDefinition(
|
|
340
|
+
options?: CreateSkillsConfigDefinitionOptions,
|
|
341
|
+
): UnifiedConfigDefinition<"skill", SkillConfig> {
|
|
342
|
+
const directories =
|
|
343
|
+
options?.directories ??
|
|
344
|
+
resolveSkillsConfigSearchPaths(options?.workspacePath);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
type: "skill",
|
|
348
|
+
directories,
|
|
349
|
+
discoverFiles: discoverSkillFiles,
|
|
350
|
+
includeFile: (fileName) => fileName === SKILL_FILE_NAME,
|
|
351
|
+
parseFile: (context) =>
|
|
352
|
+
parseSkillConfigFromMarkdown(
|
|
353
|
+
context.content,
|
|
354
|
+
basename(context.directoryPath),
|
|
355
|
+
),
|
|
356
|
+
resolveId: (skill) => normalizeName(skill.name),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function createRulesConfigDefinition(
|
|
361
|
+
options?: CreateRulesConfigDefinitionOptions,
|
|
362
|
+
): UnifiedConfigDefinition<"rule", RuleConfig> {
|
|
363
|
+
const directories =
|
|
364
|
+
options?.directories ??
|
|
365
|
+
resolveRulesConfigSearchPaths(options?.workspacePath);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
type: "rule",
|
|
369
|
+
directories,
|
|
370
|
+
discoverFiles: discoverRulesLikeFiles,
|
|
371
|
+
includeFile: (fileName, filePath) =>
|
|
372
|
+
fileName === ".clinerules" ||
|
|
373
|
+
isMarkdownFile(fileName) ||
|
|
374
|
+
isMarkdownFile(filePath),
|
|
375
|
+
parseFile: (context) =>
|
|
376
|
+
parseRuleConfigFromMarkdown(
|
|
377
|
+
context.content,
|
|
378
|
+
basename(context.filePath, extname(context.filePath)),
|
|
379
|
+
),
|
|
380
|
+
resolveId: (rule) => normalizeName(rule.name),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function createWorkflowsConfigDefinition(
|
|
385
|
+
options?: CreateWorkflowsConfigDefinitionOptions,
|
|
386
|
+
): UnifiedConfigDefinition<"workflow", WorkflowConfig> {
|
|
387
|
+
const directories =
|
|
388
|
+
options?.directories ??
|
|
389
|
+
resolveWorkflowsConfigSearchPaths(options?.workspacePath);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
type: "workflow",
|
|
393
|
+
directories,
|
|
394
|
+
discoverFiles: discoverRulesLikeFiles,
|
|
395
|
+
includeFile: (fileName) => isMarkdownFile(fileName),
|
|
396
|
+
parseFile: (context) =>
|
|
397
|
+
parseWorkflowConfigFromMarkdown(
|
|
398
|
+
context.content,
|
|
399
|
+
basename(context.filePath, extname(context.filePath)),
|
|
400
|
+
),
|
|
401
|
+
resolveId: (workflow) => normalizeName(workflow.name),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface CreateUserInstructionConfigWatcherOptions
|
|
406
|
+
extends CreateInstructionWatcherOptions {
|
|
407
|
+
skills?: CreateSkillsConfigDefinitionOptions;
|
|
408
|
+
rules?: CreateRulesConfigDefinitionOptions;
|
|
409
|
+
workflows?: CreateWorkflowsConfigDefinitionOptions;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function createUserInstructionConfigWatcher(
|
|
413
|
+
options?: CreateUserInstructionConfigWatcherOptions,
|
|
414
|
+
): UserInstructionConfigWatcher {
|
|
415
|
+
const definitions: ReadonlyArray<
|
|
416
|
+
UnifiedConfigDefinition<UserInstructionConfigType, UserInstructionConfig>
|
|
417
|
+
> = [
|
|
418
|
+
createSkillsConfigDefinition(options?.skills) as UnifiedConfigDefinition<
|
|
419
|
+
UserInstructionConfigType,
|
|
420
|
+
UserInstructionConfig
|
|
421
|
+
>,
|
|
422
|
+
createRulesConfigDefinition(options?.rules) as UnifiedConfigDefinition<
|
|
423
|
+
UserInstructionConfigType,
|
|
424
|
+
UserInstructionConfig
|
|
425
|
+
>,
|
|
426
|
+
createWorkflowsConfigDefinition(
|
|
427
|
+
options?.workflows,
|
|
428
|
+
) as UnifiedConfigDefinition<
|
|
429
|
+
UserInstructionConfigType,
|
|
430
|
+
UserInstructionConfig
|
|
431
|
+
>,
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
return new UnifiedConfigFileWatcher(definitions, {
|
|
435
|
+
debounceMs: options?.debounceMs,
|
|
436
|
+
emitParseErrors: options?.emitParseErrors,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { createOAuthClientCallbacks } from "./client.js";
|
|
3
|
+
|
|
4
|
+
describe("auth/client createOAuthClientCallbacks", () => {
|
|
5
|
+
it("emits instructions and URL and forwards prompts", async () => {
|
|
6
|
+
const onOutput = vi.fn();
|
|
7
|
+
const onPrompt = vi.fn().mockResolvedValue("value");
|
|
8
|
+
const callbacks = createOAuthClientCallbacks({ onOutput, onPrompt });
|
|
9
|
+
|
|
10
|
+
callbacks.onAuth({
|
|
11
|
+
url: "https://example.com/auth",
|
|
12
|
+
instructions: "Open your browser",
|
|
13
|
+
});
|
|
14
|
+
const answer = await callbacks.onPrompt({ message: "Enter code" });
|
|
15
|
+
|
|
16
|
+
expect(answer).toBe("value");
|
|
17
|
+
expect(onPrompt).toHaveBeenCalledWith({ message: "Enter code" });
|
|
18
|
+
expect(onOutput).toHaveBeenNthCalledWith(1, "Open your browser");
|
|
19
|
+
expect(onOutput).toHaveBeenNthCalledWith(2, "https://example.com/auth");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("tries opening URL and reports opener errors", async () => {
|
|
23
|
+
const openUrl = vi.fn().mockRejectedValue(new Error("failed"));
|
|
24
|
+
const onOpenUrlError = vi.fn();
|
|
25
|
+
const callbacks = createOAuthClientCallbacks({
|
|
26
|
+
onPrompt: vi.fn().mockResolvedValue(""),
|
|
27
|
+
openUrl,
|
|
28
|
+
onOpenUrlError,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
callbacks.onAuth({ url: "https://example.com/auth" });
|
|
32
|
+
await Promise.resolve();
|
|
33
|
+
|
|
34
|
+
expect(openUrl).toHaveBeenCalledWith("https://example.com/auth");
|
|
35
|
+
expect(onOpenUrlError).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(onOpenUrlError.mock.calls[0]?.[0]).toMatchObject({
|
|
37
|
+
url: "https://example.com/auth",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OAuthLoginCallbacks, OAuthPrompt } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface OAuthClientCallbacksOptions {
|
|
4
|
+
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
|
5
|
+
onOutput?: (message: string) => void;
|
|
6
|
+
openUrl?: (url: string) => void | Promise<void>;
|
|
7
|
+
onOpenUrlError?: (context: { url: string; error: unknown }) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createOAuthClientCallbacks(
|
|
11
|
+
options: OAuthClientCallbacksOptions,
|
|
12
|
+
): OAuthLoginCallbacks {
|
|
13
|
+
return {
|
|
14
|
+
onAuth: ({ url, instructions }) => {
|
|
15
|
+
options.onOutput?.(instructions ?? "Complete sign-in in your browser.");
|
|
16
|
+
if (options.openUrl) {
|
|
17
|
+
void Promise.resolve(options.openUrl(url)).catch((error) => {
|
|
18
|
+
options.onOpenUrlError?.({ url, error });
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
options.onOutput?.(url);
|
|
22
|
+
},
|
|
23
|
+
onPrompt: options.onPrompt,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ClineOAuthCredentials } from "./cline.js";
|
|
3
|
+
import { getValidClineCredentials } from "./cline.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_OPTIONS = {
|
|
6
|
+
apiBaseUrl: "https://auth.example.com",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function createCredentials(
|
|
10
|
+
overrides: Partial<ClineOAuthCredentials> = {},
|
|
11
|
+
): ClineOAuthCredentials {
|
|
12
|
+
return {
|
|
13
|
+
access: "access-old",
|
|
14
|
+
refresh: "refresh-old",
|
|
15
|
+
expires: 0,
|
|
16
|
+
accountId: "acct-1",
|
|
17
|
+
email: "user@example.com",
|
|
18
|
+
metadata: { provider: "google" },
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("auth/cline getValidClineCredentials", () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.unstubAllGlobals();
|
|
26
|
+
vi.restoreAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns existing credentials when not expired", async () => {
|
|
30
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(10_000);
|
|
31
|
+
const current = createCredentials({ expires: 400_000 });
|
|
32
|
+
const fetchMock = vi.fn();
|
|
33
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
34
|
+
|
|
35
|
+
const result = await getValidClineCredentials(current, PROVIDER_OPTIONS);
|
|
36
|
+
expect(result).toBe(current);
|
|
37
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
38
|
+
nowSpy.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("refreshes expired credentials", async () => {
|
|
42
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(100_000);
|
|
43
|
+
const current = createCredentials({ expires: 101_000 });
|
|
44
|
+
const fetchMock = vi.fn(
|
|
45
|
+
async () =>
|
|
46
|
+
new Response(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
success: true,
|
|
49
|
+
data: {
|
|
50
|
+
accessToken: "access-new",
|
|
51
|
+
refreshToken: "refresh-new",
|
|
52
|
+
tokenType: "Bearer",
|
|
53
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
54
|
+
userInfo: {
|
|
55
|
+
subject: "sub-1",
|
|
56
|
+
email: "new@example.com",
|
|
57
|
+
name: "New User",
|
|
58
|
+
clineUserId: "acct-2",
|
|
59
|
+
accounts: [],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
67
|
+
|
|
68
|
+
const result = await getValidClineCredentials(current, PROVIDER_OPTIONS);
|
|
69
|
+
expect(result).toMatchObject({
|
|
70
|
+
access: "access-new",
|
|
71
|
+
refresh: "refresh-new",
|
|
72
|
+
accountId: "acct-2",
|
|
73
|
+
email: "new@example.com",
|
|
74
|
+
});
|
|
75
|
+
nowSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns null when refresh fails with invalid_grant", async () => {
|
|
79
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(100_000);
|
|
80
|
+
const current = createCredentials({ expires: 101_000 });
|
|
81
|
+
vi.stubGlobal(
|
|
82
|
+
"fetch",
|
|
83
|
+
vi.fn(
|
|
84
|
+
async () =>
|
|
85
|
+
new Response(
|
|
86
|
+
JSON.stringify({
|
|
87
|
+
error: "invalid_grant",
|
|
88
|
+
error_description: "refresh expired",
|
|
89
|
+
}),
|
|
90
|
+
{
|
|
91
|
+
status: 401,
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
},
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result = await getValidClineCredentials(current, PROVIDER_OPTIONS);
|
|
99
|
+
expect(result).toBeNull();
|
|
100
|
+
nowSpy.mockRestore();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("keeps current credentials on transient refresh error while token remains valid", async () => {
|
|
104
|
+
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(100_000);
|
|
105
|
+
const current = createCredentials({ expires: 150_000 });
|
|
106
|
+
vi.stubGlobal(
|
|
107
|
+
"fetch",
|
|
108
|
+
vi.fn(
|
|
109
|
+
async () =>
|
|
110
|
+
new Response(
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
error: "server_error",
|
|
113
|
+
error_description: "temporary issue",
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
status: 500,
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const result = await getValidClineCredentials(current, PROVIDER_OPTIONS, {
|
|
124
|
+
refreshBufferMs: 60_000,
|
|
125
|
+
retryableTokenGraceMs: 30_000,
|
|
126
|
+
});
|
|
127
|
+
expect(result).toBe(current);
|
|
128
|
+
nowSpy.mockRestore();
|
|
129
|
+
});
|
|
130
|
+
});
|