@clinebot/core 0.0.12 → 0.0.14
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 +1 -1
- package/dist/agents/plugin-loader.d.ts +1 -0
- package/dist/index.node.d.ts +4 -0
- package/dist/index.node.js +216 -195
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/session/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +6 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/tools/schemas.d.ts +1 -0
- package/package.json +7 -5
- package/src/agents/plugin-loader.test.ts +11 -11
- package/src/agents/plugin-loader.ts +5 -3
- package/src/auth/cline.ts +1 -29
- package/src/index.node.ts +10 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/index.ts +10 -0
- package/src/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +32 -32
- package/src/session/default-session-manager.ts +10 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-service.ts +127 -64
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +114 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/tools/definitions.test.ts +50 -0
- package/src/tools/definitions.ts +26 -0
- package/src/tools/schemas.ts +5 -6
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
export type RuntimeCommandKind = "skill" | "workflow";
|
|
3
|
+
export type AvailableRuntimeCommand = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
instructions: string;
|
|
7
|
+
kind: RuntimeCommandKind;
|
|
8
|
+
};
|
|
9
|
+
export declare function listAvailableRuntimeCommandsFromWatcher(watcher: UserInstructionConfigWatcher): AvailableRuntimeCommand[];
|
|
10
|
+
export declare function resolveRuntimeSlashCommandFromWatcher(input: string, watcher: UserInstructionConfigWatcher): string;
|
|
11
|
+
export declare function listAvailableRuntimeCommandsForKindFromWatcher(watcher: UserInstructionConfigWatcher, kind: RuntimeCommandKind): AvailableRuntimeCommand[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
export type AvailableSkill = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
instructions: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function listAvailableSkillsFromWatcher(watcher: UserInstructionConfigWatcher): AvailableSkill[];
|
|
8
|
+
/**
|
|
9
|
+
* Expands a leading slash command (e.g. "/release") to skill instructions.
|
|
10
|
+
* If the input starts with "/<skill-name>", that prefix is replaced and the
|
|
11
|
+
* remaining input is preserved unchanged.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveSkillsSlashCommandFromWatcher(input: string, watcher: UserInstructionConfigWatcher): string;
|
|
@@ -3,35 +3,35 @@ import type { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
|
3
3
|
import type { SessionSource, SessionStatus } from "../types/common";
|
|
4
4
|
import type { SessionManifest } from "./session-manifest";
|
|
5
5
|
import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
|
|
6
|
-
export interface
|
|
7
|
-
|
|
6
|
+
export interface SessionRow {
|
|
7
|
+
sessionId: string;
|
|
8
8
|
source: string;
|
|
9
9
|
pid: number;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
startedAt: string;
|
|
11
|
+
endedAt?: string | null;
|
|
12
|
+
exitCode?: number | null;
|
|
13
13
|
status: SessionStatus;
|
|
14
|
-
|
|
15
|
-
interactive:
|
|
14
|
+
statusLock: number;
|
|
15
|
+
interactive: boolean;
|
|
16
16
|
provider: string;
|
|
17
17
|
model: string;
|
|
18
18
|
cwd: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
workspaceRoot: string;
|
|
20
|
+
teamName?: string | null;
|
|
21
|
+
enableTools: boolean;
|
|
22
|
+
enableSpawn: boolean;
|
|
23
|
+
enableTeams: boolean;
|
|
24
|
+
parentSessionId?: string | null;
|
|
25
|
+
parentAgentId?: string | null;
|
|
26
|
+
agentId?: string | null;
|
|
27
|
+
conversationId?: string | null;
|
|
28
|
+
isSubagent: boolean;
|
|
29
29
|
prompt?: string | null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
metadata?: Record<string, unknown> | null;
|
|
31
|
+
transcriptPath: string;
|
|
32
|
+
hookPath: string;
|
|
33
|
+
messagesPath?: string | null;
|
|
34
|
+
updatedAt: string;
|
|
35
35
|
}
|
|
36
36
|
export interface CreateRootSessionInput {
|
|
37
37
|
sessionId: string;
|
|
@@ -3,7 +3,7 @@ import type { LlmsProviders } from "@clinebot/llms";
|
|
|
3
3
|
import type { SessionStatus } from "../types/common";
|
|
4
4
|
import { SessionArtifacts } from "./session-artifacts";
|
|
5
5
|
import { type SessionManifest } from "./session-manifest";
|
|
6
|
-
import type { CreateRootSessionWithArtifactsInput, RootSessionArtifacts,
|
|
6
|
+
import type { CreateRootSessionWithArtifactsInput, RootSessionArtifacts, SessionRow, UpsertSubagentInput } from "./session-service";
|
|
7
7
|
export interface PersistedSessionUpdateInput {
|
|
8
8
|
sessionId: string;
|
|
9
9
|
expectedStatusLock?: number;
|
|
@@ -11,7 +11,7 @@ export interface PersistedSessionUpdateInput {
|
|
|
11
11
|
endedAt?: string | null;
|
|
12
12
|
exitCode?: number | null;
|
|
13
13
|
prompt?: string | null;
|
|
14
|
-
|
|
14
|
+
metadata?: Record<string, unknown> | null;
|
|
15
15
|
title?: string | null;
|
|
16
16
|
parentSessionId?: string | null;
|
|
17
17
|
parentAgentId?: string | null;
|
|
@@ -21,13 +21,13 @@ export interface PersistedSessionUpdateInput {
|
|
|
21
21
|
}
|
|
22
22
|
export interface SessionPersistenceAdapter {
|
|
23
23
|
ensureSessionsDir(): string;
|
|
24
|
-
upsertSession(row:
|
|
25
|
-
getSession(sessionId: string): Promise<
|
|
24
|
+
upsertSession(row: SessionRow): Promise<void>;
|
|
25
|
+
getSession(sessionId: string): Promise<SessionRow | undefined>;
|
|
26
26
|
listSessions(options: {
|
|
27
27
|
limit: number;
|
|
28
28
|
parentSessionId?: string;
|
|
29
29
|
status?: string;
|
|
30
|
-
}): Promise<
|
|
30
|
+
}): Promise<SessionRow[]>;
|
|
31
31
|
updateSession(input: PersistedSessionUpdateInput): Promise<{
|
|
32
32
|
updated: boolean;
|
|
33
33
|
statusLock: number;
|
|
@@ -91,7 +91,7 @@ export declare class UnifiedSessionPersistenceService {
|
|
|
91
91
|
handleSubAgentEnd(rootSessionId: string, context: SubAgentEndContext): Promise<void>;
|
|
92
92
|
private isPidAlive;
|
|
93
93
|
private reconcileDeadRunningSession;
|
|
94
|
-
listSessions(limit?: number): Promise<
|
|
94
|
+
listSessions(limit?: number): Promise<SessionRow[]>;
|
|
95
95
|
reconcileDeadSessions(limit?: number): Promise<number>;
|
|
96
96
|
deleteSession(sessionId: string): Promise<{
|
|
97
97
|
deleted: boolean;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AgentConfig, AgentEvent, AgentResult } from "@clinebot/agents";
|
|
2
2
|
import type { LlmsProviders } from "@clinebot/llms";
|
|
3
3
|
import type { SessionRecord } from "../../types/sessions";
|
|
4
|
-
import type {
|
|
4
|
+
import type { SessionRow } from "../session-service";
|
|
5
5
|
import type { StoredMessageWithMetadata } from "./types";
|
|
6
6
|
export declare function extractWorkspaceMetadataFromSystemPrompt(systemPrompt: string): string | undefined;
|
|
7
7
|
export declare function hasRuntimeHooks(hooks: AgentConfig["hooks"]): boolean;
|
|
8
8
|
export declare function mergeAgentExtensions(explicitExtensions: AgentConfig["extensions"] | undefined, loadedExtensions: AgentConfig["extensions"] | undefined): AgentConfig["extensions"];
|
|
9
9
|
export declare function serializeAgentEvent(event: AgentEvent): string;
|
|
10
10
|
export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult, previousMessages?: LlmsProviders.MessageWithMetadata[]): StoredMessageWithMetadata[];
|
|
11
|
-
export declare function toSessionRecord(row:
|
|
11
|
+
export declare function toSessionRecord(row: SessionRow): SessionRecord;
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and are used for both validation and JSON Schema generation.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
export declare const INPUT_ARG_CHAR_LIMIT = 6000;
|
|
8
9
|
export declare const ReadFileLineRangeSchema: z.ZodObject<{
|
|
9
10
|
start_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
10
11
|
end_line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"main": "./dist/index.node.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@clinebot/agents": "0.0.
|
|
7
|
-
"@clinebot/llms": "0.0.
|
|
8
|
-
"@clinebot/shared": "0.0.
|
|
6
|
+
"@clinebot/agents": "0.0.14",
|
|
7
|
+
"@clinebot/llms": "0.0.14",
|
|
8
|
+
"@clinebot/shared": "0.0.14",
|
|
9
9
|
"@opentelemetry/api": "^1.9.0",
|
|
10
10
|
"@opentelemetry/api-logs": "^0.56.0",
|
|
11
11
|
"@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"@opentelemetry/sdk-logs": "^0.56.0",
|
|
15
15
|
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
16
16
|
"@opentelemetry/semantic-conventions": "^1.37.0",
|
|
17
|
-
"better-sqlite3": "^11.10.0",
|
|
18
17
|
"jiti": "^1.21.7",
|
|
19
18
|
"nanoid": "^5.1.7",
|
|
20
19
|
"simple-git": "^3.32.3",
|
|
@@ -52,6 +51,9 @@
|
|
|
52
51
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
53
52
|
"test:watch": "vitest --config vitest.config.ts"
|
|
54
53
|
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20"
|
|
56
|
+
},
|
|
55
57
|
"type": "module",
|
|
56
58
|
"types": "./dist/index.node.d.ts"
|
|
57
59
|
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { mkdir, mkdtemp,
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { join } from "node:path";
|
|
5
4
|
import { describe, expect, it } from "vitest";
|
|
6
5
|
import {
|
|
7
6
|
loadAgentPluginFromPath,
|
|
8
7
|
loadAgentPluginsFromPaths,
|
|
9
8
|
} from "./plugin-loader";
|
|
10
9
|
|
|
11
|
-
const TEST_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
12
|
-
const REPO_ROOT = resolve(TEST_DIR, "..", "..", "..", "..");
|
|
13
|
-
|
|
14
10
|
describe("plugin-loader", () => {
|
|
15
11
|
it("loads default-exported plugin from path", async () => {
|
|
16
12
|
const dir = await mkdtemp(join(tmpdir(), "core-plugin-loader-"));
|
|
@@ -193,15 +189,19 @@ describe("plugin-loader", () => {
|
|
|
193
189
|
const pluginPath = join(dir, "portable-subagents.ts");
|
|
194
190
|
await writeFile(
|
|
195
191
|
pluginPath,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"
|
|
199
|
-
|
|
192
|
+
[
|
|
193
|
+
"import { resolveClineDataDir } from '@clinebot/shared/storage';",
|
|
194
|
+
"import YAML from 'yaml';",
|
|
195
|
+
"export default {",
|
|
196
|
+
" name: typeof resolveClineDataDir === 'function' ? YAML.stringify({ ok: true }) : 'invalid',",
|
|
197
|
+
" manifest: { capabilities: ['tools'] },",
|
|
198
|
+
"};",
|
|
199
|
+
].join("\n"),
|
|
200
200
|
"utf8",
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
await expect(
|
|
204
|
-
loadAgentPluginFromPath(pluginPath, { cwd: dir }),
|
|
204
|
+
loadAgentPluginFromPath(pluginPath, { cwd: dir, useCache: true }),
|
|
205
205
|
).rejects.toThrow(/Cannot find (package|module) 'yaml'/i);
|
|
206
206
|
} finally {
|
|
207
207
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -17,6 +17,7 @@ type PluginLike = {
|
|
|
17
17
|
export interface LoadAgentPluginFromPathOptions {
|
|
18
18
|
exportName?: string;
|
|
19
19
|
cwd?: string;
|
|
20
|
+
useCache?: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
@@ -130,12 +131,13 @@ function collectPluginImportAliases(
|
|
|
130
131
|
|
|
131
132
|
async function importPluginModule(
|
|
132
133
|
absolutePath: string,
|
|
134
|
+
options: LoadAgentPluginFromPathOptions = {},
|
|
133
135
|
): Promise<Record<string, unknown>> {
|
|
134
136
|
const aliases = collectPluginImportAliases(absolutePath);
|
|
135
137
|
const jiti = createJiti(absolutePath, {
|
|
136
138
|
alias: aliases,
|
|
137
|
-
cache:
|
|
138
|
-
requireCache:
|
|
139
|
+
cache: options.useCache,
|
|
140
|
+
requireCache: options.useCache,
|
|
139
141
|
esmResolve: true,
|
|
140
142
|
interopDefault: false,
|
|
141
143
|
nativeModules: [...BUILTIN_MODULES],
|
|
@@ -149,7 +151,7 @@ export async function loadAgentPluginFromPath(
|
|
|
149
151
|
options: LoadAgentPluginFromPathOptions = {},
|
|
150
152
|
): Promise<AgentPlugin> {
|
|
151
153
|
const absolutePath = resolve(options.cwd ?? process.cwd(), pluginPath);
|
|
152
|
-
const moduleExports = await importPluginModule(absolutePath);
|
|
154
|
+
const moduleExports = await importPluginModule(absolutePath, options);
|
|
153
155
|
const exportName = options.exportName ?? "plugin";
|
|
154
156
|
const plugin = (moduleExports.default ??
|
|
155
157
|
moduleExports[exportName]) as unknown;
|
package/src/auth/cline.ts
CHANGED
|
@@ -180,35 +180,7 @@ async function requestAuthorizationUrl(
|
|
|
180
180
|
authUrl.searchParams.set("redirect_uri", params.callbackUrl);
|
|
181
181
|
authUrl.searchParams.set("state", params.state);
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
method: "GET",
|
|
185
|
-
redirect: "manual",
|
|
186
|
-
headers: await resolveHeaders(options.headers),
|
|
187
|
-
signal: AbortSignal.timeout(
|
|
188
|
-
options.requestTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
|
|
189
|
-
),
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
if (response.status >= 300 && response.status < 400) {
|
|
193
|
-
const redirectUrl = response.headers.get("location");
|
|
194
|
-
if (redirectUrl) {
|
|
195
|
-
return redirectUrl;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
const text = await response.text().catch(() => "");
|
|
201
|
-
throw new Error(
|
|
202
|
-
`Authentication request failed: ${response.status} ${text}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const body = (await response.json()) as { redirect_url?: string };
|
|
207
|
-
if (typeof body.redirect_url === "string" && body.redirect_url.length > 0) {
|
|
208
|
-
return body.redirect_url;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
throw new Error("Authentication request did not return a redirect URL");
|
|
183
|
+
return authUrl.toString();
|
|
212
184
|
}
|
|
213
185
|
|
|
214
186
|
async function exchangeAuthorizationCode(
|
package/src/index.node.ts
CHANGED
|
@@ -138,6 +138,11 @@ export {
|
|
|
138
138
|
saveLocalProviderOAuthCredentials,
|
|
139
139
|
saveLocalProviderSettings,
|
|
140
140
|
} from "./providers/local-provider-service";
|
|
141
|
+
export type { AvailableRuntimeCommand } from "./runtime/commands";
|
|
142
|
+
export {
|
|
143
|
+
listAvailableRuntimeCommandsFromWatcher,
|
|
144
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
145
|
+
} from "./runtime/commands";
|
|
141
146
|
export {
|
|
142
147
|
formatRulesForSystemPrompt,
|
|
143
148
|
isRuleEnabled,
|
|
@@ -159,6 +164,11 @@ export type {
|
|
|
159
164
|
RuntimeBuilderInput,
|
|
160
165
|
SessionRuntime,
|
|
161
166
|
} from "./runtime/session-runtime";
|
|
167
|
+
export type { AvailableSkill } from "./runtime/skills";
|
|
168
|
+
export {
|
|
169
|
+
listAvailableSkillsFromWatcher,
|
|
170
|
+
resolveSkillsSlashCommandFromWatcher,
|
|
171
|
+
} from "./runtime/skills";
|
|
162
172
|
export {
|
|
163
173
|
type DesktopToolApprovalOptions,
|
|
164
174
|
requestDesktopToolApproval,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createUserInstructionConfigWatcher } from "../agents";
|
|
6
|
+
import {
|
|
7
|
+
listAvailableRuntimeCommandsFromWatcher,
|
|
8
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
9
|
+
} from "./commands";
|
|
10
|
+
|
|
11
|
+
describe("runtime command registry", () => {
|
|
12
|
+
const tempRoots: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await Promise.all(
|
|
16
|
+
tempRoots.map((dir) => rm(dir, { recursive: true, force: true })),
|
|
17
|
+
);
|
|
18
|
+
tempRoots.length = 0;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("lists workflow and skill commands together", async () => {
|
|
22
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-commands-"));
|
|
23
|
+
tempRoots.push(tempRoot);
|
|
24
|
+
const skillDir = join(tempRoot, "skills", "debug");
|
|
25
|
+
const workflowsDir = join(tempRoot, "workflows");
|
|
26
|
+
await mkdir(skillDir, { recursive: true });
|
|
27
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
28
|
+
await writeFile(join(skillDir, "SKILL.md"), "Use the debugging skill.");
|
|
29
|
+
await writeFile(
|
|
30
|
+
join(workflowsDir, "release.md"),
|
|
31
|
+
`---
|
|
32
|
+
name: release
|
|
33
|
+
---
|
|
34
|
+
Run the release workflow.`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const watcher = createUserInstructionConfigWatcher({
|
|
38
|
+
skills: { directories: [join(tempRoot, "skills")] },
|
|
39
|
+
rules: { directories: [] },
|
|
40
|
+
workflows: { directories: [workflowsDir] },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await watcher.start();
|
|
45
|
+
expect(listAvailableRuntimeCommandsFromWatcher(watcher)).toEqual([
|
|
46
|
+
{
|
|
47
|
+
id: "debug",
|
|
48
|
+
name: "debug",
|
|
49
|
+
instructions: "Use the debugging skill.",
|
|
50
|
+
kind: "skill",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "release",
|
|
54
|
+
name: "release",
|
|
55
|
+
instructions: "Run the release workflow.",
|
|
56
|
+
kind: "workflow",
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
} finally {
|
|
60
|
+
watcher.stop();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("expands skill and workflow slash commands with workflow precedence", async () => {
|
|
65
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "core-runtime-commands-"));
|
|
66
|
+
tempRoots.push(tempRoot);
|
|
67
|
+
const skillDir = join(tempRoot, "skills", "ship");
|
|
68
|
+
const workflowsDir = join(tempRoot, "workflows");
|
|
69
|
+
await mkdir(skillDir, { recursive: true });
|
|
70
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
71
|
+
await writeFile(join(skillDir, "SKILL.md"), "Use the ship skill.");
|
|
72
|
+
await writeFile(
|
|
73
|
+
join(workflowsDir, "ship.md"),
|
|
74
|
+
`---
|
|
75
|
+
name: ship
|
|
76
|
+
---
|
|
77
|
+
Run the ship workflow.`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const watcher = createUserInstructionConfigWatcher({
|
|
81
|
+
skills: { directories: [join(tempRoot, "skills")] },
|
|
82
|
+
rules: { directories: [] },
|
|
83
|
+
workflows: { directories: [workflowsDir] },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await watcher.start();
|
|
88
|
+
expect(resolveRuntimeSlashCommandFromWatcher("/ship", watcher)).toBe(
|
|
89
|
+
"Run the ship workflow.",
|
|
90
|
+
);
|
|
91
|
+
expect(resolveRuntimeSlashCommandFromWatcher("/ship now", watcher)).toBe(
|
|
92
|
+
"Run the ship workflow. now",
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
watcher.stop();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SkillConfig,
|
|
3
|
+
UserInstructionConfigWatcher,
|
|
4
|
+
WorkflowConfig,
|
|
5
|
+
} from "../agents";
|
|
6
|
+
|
|
7
|
+
export type RuntimeCommandKind = "skill" | "workflow";
|
|
8
|
+
|
|
9
|
+
export type AvailableRuntimeCommand = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
instructions: string;
|
|
13
|
+
kind: RuntimeCommandKind;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CommandRecord = {
|
|
17
|
+
item: SkillConfig | WorkflowConfig;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function isCommandEnabled(command: SkillConfig | WorkflowConfig): boolean {
|
|
21
|
+
return command.disabled !== true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function listCommandsForKind(
|
|
25
|
+
watcher: UserInstructionConfigWatcher,
|
|
26
|
+
kind: RuntimeCommandKind,
|
|
27
|
+
): AvailableRuntimeCommand[] {
|
|
28
|
+
return [...watcher.getSnapshot(kind).entries()]
|
|
29
|
+
.map(([id, record]) => ({ id, record: record as CommandRecord }))
|
|
30
|
+
.filter(({ record }) => isCommandEnabled(record.item))
|
|
31
|
+
.map(({ id, record }) => ({
|
|
32
|
+
id,
|
|
33
|
+
name: record.item.name,
|
|
34
|
+
instructions: record.item.instructions,
|
|
35
|
+
kind,
|
|
36
|
+
}))
|
|
37
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listAvailableRuntimeCommandsFromWatcher(
|
|
41
|
+
watcher: UserInstructionConfigWatcher,
|
|
42
|
+
): AvailableRuntimeCommand[] {
|
|
43
|
+
const byName = new Map<string, AvailableRuntimeCommand>();
|
|
44
|
+
for (const command of [
|
|
45
|
+
...listCommandsForKind(watcher, "workflow"),
|
|
46
|
+
...listCommandsForKind(watcher, "skill"),
|
|
47
|
+
]) {
|
|
48
|
+
if (!byName.has(command.name)) {
|
|
49
|
+
byName.set(command.name, command);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function resolveRuntimeSlashCommandFromWatcher(
|
|
56
|
+
input: string,
|
|
57
|
+
watcher: UserInstructionConfigWatcher,
|
|
58
|
+
): string {
|
|
59
|
+
if (!input.startsWith("/") || input.length < 2) {
|
|
60
|
+
return input;
|
|
61
|
+
}
|
|
62
|
+
const match = input.match(/^\/(\S+)/);
|
|
63
|
+
if (!match) {
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
const name = match[1];
|
|
67
|
+
if (!name) {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
const commandLength = name.length + 1;
|
|
71
|
+
const remainder = input.slice(commandLength);
|
|
72
|
+
const matched = listAvailableRuntimeCommandsFromWatcher(watcher).find(
|
|
73
|
+
(command) => command.name === name,
|
|
74
|
+
);
|
|
75
|
+
return matched ? `${matched.instructions}${remainder}` : input;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function listAvailableRuntimeCommandsForKindFromWatcher(
|
|
79
|
+
watcher: UserInstructionConfigWatcher,
|
|
80
|
+
kind: RuntimeCommandKind,
|
|
81
|
+
): AvailableRuntimeCommand[] {
|
|
82
|
+
return listCommandsForKind(watcher, kind);
|
|
83
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type AvailableRuntimeCommand,
|
|
3
|
+
listAvailableRuntimeCommandsFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
1
6
|
export {
|
|
2
7
|
formatRulesForSystemPrompt,
|
|
3
8
|
isRuleEnabled,
|
|
@@ -16,6 +21,11 @@ export type {
|
|
|
16
21
|
RuntimeBuilderInput,
|
|
17
22
|
SessionRuntime,
|
|
18
23
|
} from "./session-runtime";
|
|
24
|
+
export {
|
|
25
|
+
type AvailableSkill,
|
|
26
|
+
listAvailableSkillsFromWatcher,
|
|
27
|
+
resolveSkillsSlashCommandFromWatcher,
|
|
28
|
+
} from "./skills";
|
|
19
29
|
export {
|
|
20
30
|
type DesktopToolApprovalOptions,
|
|
21
31
|
requestDesktopToolApproval,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
import {
|
|
3
|
+
listAvailableRuntimeCommandsForKindFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
6
|
+
|
|
7
|
+
export type AvailableSkill = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
instructions: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function matchesLeadingSlashCommand(input: string, name: string): boolean {
|
|
14
|
+
const match = input.match(/^\/(\S+)/);
|
|
15
|
+
return match?.[1] === name;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function listAvailableSkillsFromWatcher(
|
|
19
|
+
watcher: UserInstructionConfigWatcher,
|
|
20
|
+
): AvailableSkill[] {
|
|
21
|
+
return listAvailableRuntimeCommandsForKindFromWatcher(watcher, "skill").map(
|
|
22
|
+
(skill) => ({
|
|
23
|
+
id: skill.id,
|
|
24
|
+
name: skill.name,
|
|
25
|
+
instructions: skill.instructions,
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Expands a leading slash command (e.g. "/release") to skill instructions.
|
|
32
|
+
* If the input starts with "/<skill-name>", that prefix is replaced and the
|
|
33
|
+
* remaining input is preserved unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSkillsSlashCommandFromWatcher(
|
|
36
|
+
input: string,
|
|
37
|
+
watcher: UserInstructionConfigWatcher,
|
|
38
|
+
): string {
|
|
39
|
+
const resolved = resolveRuntimeSlashCommandFromWatcher(input, watcher);
|
|
40
|
+
const matched = listAvailableSkillsFromWatcher(watcher).some((skill) =>
|
|
41
|
+
matchesLeadingSlashCommand(input, skill.name),
|
|
42
|
+
);
|
|
43
|
+
return matched ? resolved : input;
|
|
44
|
+
}
|
package/src/runtime/workflows.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { UserInstructionConfigWatcher
|
|
1
|
+
import type { UserInstructionConfigWatcher } from "../agents";
|
|
2
|
+
import {
|
|
3
|
+
listAvailableRuntimeCommandsForKindFromWatcher,
|
|
4
|
+
resolveRuntimeSlashCommandFromWatcher,
|
|
5
|
+
} from "./commands";
|
|
2
6
|
|
|
3
7
|
export type AvailableWorkflow = {
|
|
4
8
|
id: string;
|
|
@@ -6,23 +10,22 @@ export type AvailableWorkflow = {
|
|
|
6
10
|
instructions: string;
|
|
7
11
|
};
|
|
8
12
|
|
|
9
|
-
function
|
|
10
|
-
|
|
13
|
+
function matchesLeadingSlashCommand(input: string, name: string): boolean {
|
|
14
|
+
const match = input.match(/^\/(\S+)/);
|
|
15
|
+
return match?.[1] === name;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export function listAvailableWorkflowsFromWatcher(
|
|
14
19
|
watcher: UserInstructionConfigWatcher,
|
|
15
20
|
): AvailableWorkflow[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}))
|
|
25
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
21
|
+
return listAvailableRuntimeCommandsForKindFromWatcher(
|
|
22
|
+
watcher,
|
|
23
|
+
"workflow",
|
|
24
|
+
).map((workflow) => ({
|
|
25
|
+
id: workflow.id,
|
|
26
|
+
name: workflow.name,
|
|
27
|
+
instructions: workflow.instructions,
|
|
28
|
+
}));
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -34,21 +37,9 @@ export function resolveWorkflowSlashCommandFromWatcher(
|
|
|
34
37
|
input: string,
|
|
35
38
|
watcher: UserInstructionConfigWatcher,
|
|
36
39
|
): string {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const match = input.match(/^\/(\S+)/);
|
|
41
|
-
if (!match) {
|
|
42
|
-
return input;
|
|
43
|
-
}
|
|
44
|
-
const workflowName = match[1];
|
|
45
|
-
if (!workflowName) {
|
|
46
|
-
return input;
|
|
47
|
-
}
|
|
48
|
-
const commandLength = workflowName.length + 1;
|
|
49
|
-
const remainder = input.slice(commandLength);
|
|
50
|
-
const matched = listAvailableWorkflowsFromWatcher(watcher).find(
|
|
51
|
-
(workflow) => workflow.name === workflowName,
|
|
40
|
+
const resolved = resolveRuntimeSlashCommandFromWatcher(input, watcher);
|
|
41
|
+
const matched = listAvailableWorkflowsFromWatcher(watcher).some((workflow) =>
|
|
42
|
+
matchesLeadingSlashCommand(input, workflow.name),
|
|
52
43
|
);
|
|
53
|
-
return matched ?
|
|
44
|
+
return matched ? resolved : input;
|
|
54
45
|
}
|