@capekai/core 1.0.10 → 1.0.12
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/package.json
CHANGED
package/src/adapters/ai-sdk.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
2
|
import {
|
|
3
|
+
wrapLanguageModel,
|
|
3
4
|
dynamicTool,
|
|
4
5
|
jsonSchema,
|
|
5
6
|
streamText,
|
|
6
7
|
type JSONSchema7,
|
|
7
|
-
type LanguageModel,
|
|
8
8
|
type Tool,
|
|
9
9
|
} from 'ai';
|
|
10
10
|
import { getModelWithMetadata } from '../core/model-utils';
|
|
11
11
|
import { openAiModelOmitsTemperature } from '../core/provider-utils';
|
|
12
12
|
import type { ModelFactoryResult } from '../providers/types';
|
|
13
|
+
import { codexNetworkRetryMiddleware } from './codex-network-retry';
|
|
13
14
|
|
|
14
15
|
export interface TextModelRequest {
|
|
15
16
|
modelId?: string;
|
|
@@ -55,7 +56,10 @@ export function createOpenAiResponsesModel(request: OpenAiResponsesModelRequest)
|
|
|
55
56
|
fetch: request.fetch,
|
|
56
57
|
});
|
|
57
58
|
return {
|
|
58
|
-
model:
|
|
59
|
+
model: wrapLanguageModel({
|
|
60
|
+
model: openai.responses(request.modelId),
|
|
61
|
+
middleware: codexNetworkRetryMiddleware,
|
|
62
|
+
}),
|
|
59
63
|
useProviderInstructions: true,
|
|
60
64
|
omitMaxOutputTokens: true,
|
|
61
65
|
omitTemperature: openAiModelOmitsTemperature(request.modelId),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { APICallError, type LanguageModelMiddleware } from 'ai';
|
|
2
|
+
import { ApiErrorType, classifyApiError } from '../utils/errors';
|
|
3
|
+
|
|
4
|
+
export const codexNetworkRetryMiddleware: LanguageModelMiddleware = {
|
|
5
|
+
specificationVersion: 'v3',
|
|
6
|
+
wrapStream: async ({ doStream, params }) => {
|
|
7
|
+
try {
|
|
8
|
+
return await doStream();
|
|
9
|
+
} catch (error: unknown) {
|
|
10
|
+
if (params.abortSignal?.aborted || APICallError.isInstance(error)
|
|
11
|
+
|| (error instanceof Error && error.name === 'AbortError')) throw error;
|
|
12
|
+
const classified = classifyApiError(error);
|
|
13
|
+
if (classified.type !== ApiErrorType.Network) throw error;
|
|
14
|
+
// OpenAI's early stream probe can throw a raw socket error. Normalize
|
|
15
|
+
// only before stream handoff so SDK retries this request, not prior tools.
|
|
16
|
+
throw new APICallError({
|
|
17
|
+
message: classified.message,
|
|
18
|
+
url: 'https://chatgpt.com/backend-api/codex/responses',
|
|
19
|
+
requestBodyValues: undefined,
|
|
20
|
+
cause: error,
|
|
21
|
+
isRetryable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
package/src/internal/hosts.ts
CHANGED
|
@@ -42,6 +42,7 @@ export { executeSchedulerTool } from '../scheduler/scheduler-tool';
|
|
|
42
42
|
export { executeSessionSearchTool } from '../session-search/session-search-tool';
|
|
43
43
|
export { executeSkillManageTool, buildSkillManageToolDescription } from '../skills/skill-manage-tool';
|
|
44
44
|
export { executeMemoryTool } from '../memory/memory-tool';
|
|
45
|
+
export { withKnowledgeMutationLock } from '../runtime/knowledge-mutation-lock';
|
|
45
46
|
export {
|
|
46
47
|
addEntry,
|
|
47
48
|
entriesToContent,
|
package/src/memory/registry.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { withKnowledgeMutationLock } from '../runtime/knowledge-mutation-lock';
|
|
4
5
|
|
|
5
6
|
const USER_FILE = 'USER.md';
|
|
6
7
|
const MEMORY_FILE = 'MEMORY.md';
|
|
@@ -72,7 +73,11 @@ function fullResult(existing: string, target: MemoryTarget, entries: string[], r
|
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
export
|
|
76
|
+
export function addEntry(basePath: string, target: MemoryTarget, content: string): Promise<MemoryActionResult> {
|
|
77
|
+
return withKnowledgeMutationLock(basePath, () => addEntryUnlocked(basePath, target, content));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function addEntryUnlocked(basePath: string, target: MemoryTarget, content: string): Promise<MemoryActionResult> {
|
|
76
81
|
const trimmed = content.trim();
|
|
77
82
|
if (!trimmed) return { success: false, error: 'Content cannot be empty.' };
|
|
78
83
|
const entry = `- ${trimmed}`;
|
|
@@ -115,7 +120,11 @@ function findOne(content: string, entries: string[], target: MemoryTarget, oldTe
|
|
|
115
120
|
return matches[0];
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
export
|
|
123
|
+
export function replaceEntry(basePath: string, target: MemoryTarget, oldText: string, content: string): Promise<MemoryActionResult> {
|
|
124
|
+
return withKnowledgeMutationLock(basePath, () => replaceEntryUnlocked(basePath, target, oldText, content));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function replaceEntryUnlocked(basePath: string, target: MemoryTarget, oldText: string, content: string): Promise<MemoryActionResult> {
|
|
119
128
|
const trimmed = content.trim();
|
|
120
129
|
if (!trimmed) return { success: false, error: 'New content cannot be empty.' };
|
|
121
130
|
const loaded = await readExisting(basePath, target);
|
|
@@ -128,7 +137,11 @@ export async function replaceEntry(basePath: string, target: MemoryTarget, oldTe
|
|
|
128
137
|
return { success: true, result: { target, action: 'replace', path: fileName(target), usage: { chars: next.length, limit: charLimit(target) }, entry: trimmed } };
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
export
|
|
140
|
+
export function removeEntry(basePath: string, target: MemoryTarget, oldText: string): Promise<MemoryActionResult> {
|
|
141
|
+
return withKnowledgeMutationLock(basePath, () => removeEntryUnlocked(basePath, target, oldText));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function removeEntryUnlocked(basePath: string, target: MemoryTarget, oldText: string): Promise<MemoryActionResult> {
|
|
132
145
|
const loaded = await readExisting(basePath, target);
|
|
133
146
|
if ('success' in loaded) return loaded;
|
|
134
147
|
const match = findOne(loaded.content, loaded.entries, target, oldText);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const tails = new Map<string, Promise<void>>();
|
|
6
|
+
const active = new AsyncLocalStorage<{ held: boolean }>();
|
|
7
|
+
|
|
8
|
+
function directoryKey(directory: string): string {
|
|
9
|
+
if (!directory.trim() || directory.includes('\0')) throw new Error('Invalid knowledge directory');
|
|
10
|
+
let current = resolve(directory);
|
|
11
|
+
const missing: string[] = [];
|
|
12
|
+
for (;;) {
|
|
13
|
+
try {
|
|
14
|
+
return join(realpathSync(current), ...missing.reverse());
|
|
15
|
+
} catch (error: unknown) {
|
|
16
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
17
|
+
const parent = dirname(current);
|
|
18
|
+
if (parent === current) throw error;
|
|
19
|
+
missing.push(basename(current));
|
|
20
|
+
current = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Serialize managed read-modify-write operations by canonical directory.
|
|
26
|
+
* Process-local only: external writers must be handled by host revision policy.
|
|
27
|
+
* Do not call managed memory/skill mutators inside this callback; they acquire
|
|
28
|
+
* the same primitive themselves. Permission waits belong outside the lock. */
|
|
29
|
+
export async function withKnowledgeMutationLock<T>(directory: string, callback: () => Promise<T>): Promise<T> {
|
|
30
|
+
if (active.getStore()?.held) throw new Error('Nested knowledge mutation locks are not supported');
|
|
31
|
+
const key = directoryKey(directory);
|
|
32
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
33
|
+
let release!: () => void;
|
|
34
|
+
const tail = new Promise<void>(resolve => { release = resolve; });
|
|
35
|
+
tails.set(key, tail);
|
|
36
|
+
await previous;
|
|
37
|
+
const state = { held: true };
|
|
38
|
+
try {
|
|
39
|
+
return await active.run(state, callback);
|
|
40
|
+
} finally {
|
|
41
|
+
state.held = false;
|
|
42
|
+
release();
|
|
43
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -4,6 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import type { PermissionAsk } from '@capekai/tool';
|
|
5
5
|
import { PermissionRiskLevel } from '@capekai/tool';
|
|
6
6
|
import { scanSkillsFromDir } from './registry';
|
|
7
|
+
import { withKnowledgeMutationLock } from '../runtime/knowledge-mutation-lock';
|
|
7
8
|
|
|
8
9
|
type SkillManageAction = 'list' | 'create' | 'update' | 'patch' | 'delete';
|
|
9
10
|
export interface SkillManageResult {
|
|
@@ -137,49 +138,51 @@ export async function executeSkillManageTool(input: Record<string, unknown>, roo
|
|
|
137
138
|
if (!approved) return { success: false, error: 'USER_REJECTION' };
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
return withKnowledgeMutationLock(root, async (): Promise<SkillManageResult> => {
|
|
142
|
+
if (action === 'create') {
|
|
143
|
+
if (!description) return { success: false, error: 'description is required for create action.' };
|
|
144
|
+
if (!content) return { success: false, error: 'content is required for create action.' };
|
|
145
|
+
const path = skillPath(root, safeName);
|
|
146
|
+
if (existsSync(path)) return { success: false, error: `Skill "${safeName}" already exists. Use update or patch instead.` };
|
|
147
|
+
await mkdir(skillDir(root, safeName), { recursive: true });
|
|
148
|
+
await writeFile(path, frontmatter(safeName, description) + '\n' + content + '\n', 'utf-8');
|
|
149
|
+
return { success: true, title: `Skill created: ${safeName}`, action, name: safeName, description, path: `${safeName}/SKILL.md`, summary: 'Created workspace skill.' };
|
|
150
|
+
}
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
152
|
+
const resolved = await resolveSkillFolder(rawName, root);
|
|
153
|
+
if (!resolved) {
|
|
154
|
+
const names = await availableNames(root);
|
|
155
|
+
return { success: false, error: `Skill "${rawName}" does not exist.${names.length ? ` Available skills: ${names.join(', ')}` : action === 'delete' ? '' : ' No skills exist yet. Use create first.'}` };
|
|
156
|
+
}
|
|
157
|
+
const relativePath = `${resolved.folderName}/SKILL.md`;
|
|
158
|
+
if (action === 'delete') {
|
|
159
|
+
await rm(skillDir(root, resolved.folderName), { recursive: true, force: true });
|
|
160
|
+
return { success: true, title: `Skill deleted: ${resolved.folderName}`, action, name: resolved.folderName, path: relativePath, summary: 'Removed workspace skill directory.' };
|
|
161
|
+
}
|
|
162
|
+
let existing: string;
|
|
163
|
+
try { existing = await readFile(resolved.skillMdPath, 'utf-8'); } catch { return { success: false, error: 'Failed to read existing skill file.' }; }
|
|
164
|
+
|
|
165
|
+
if (action === 'update') {
|
|
166
|
+
if (!content) return { success: false, error: 'content is required for update action.' };
|
|
167
|
+
const existingDescription = existing.match(/^description:\s*(.+)$/m)?.[1].replace(/^['"]|['"]$/g, '') ?? '';
|
|
168
|
+
const existingName = existing.match(/^name:\s*(.+)$/m)?.[1].replace(/^['"]|['"]$/g, '').trim() ?? resolved.folderName;
|
|
169
|
+
const effectiveDescription = description ?? existingDescription;
|
|
170
|
+
await writeFile(resolved.skillMdPath, frontmatter(existingName, effectiveDescription) + '\n' + content + '\n', 'utf-8');
|
|
171
|
+
return { success: true, title: `Skill updated: ${existingName}`, action, name: existingName, description: effectiveDescription, path: relativePath, summary: 'Replaced skill body.' };
|
|
172
|
+
}
|
|
171
173
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
if (!oldString) return { success: false, error: 'oldString is required for patch action.' };
|
|
175
|
+
if (newString === undefined || newString === null) return { success: false, error: 'newString is required for patch action.' };
|
|
176
|
+
const matches = existing.split(oldString).length - 1;
|
|
177
|
+
if (matches === 0) return { success: false, error: 'oldString not found in skill file. Load the skill via the "skill" tool first to see the exact content, then copy the exact text to oldString.' };
|
|
178
|
+
if (matches > 1) return { success: false, error: `oldString matched ${matches} locations. Provide a more specific oldString.` };
|
|
179
|
+
let patched = existing.replace(oldString, newString);
|
|
180
|
+
if (description) patched = patched.replace(/^(description:\s*).*$/m, `$1${description}`);
|
|
181
|
+
await writeFile(resolved.skillMdPath, patched, 'utf-8');
|
|
182
|
+
const resultName = patched.match(/^name:\s*(.+)$/m)?.[1].replace(/^['"]|['"]$/g, '').trim() ?? resolved.folderName;
|
|
183
|
+
const resultDescription = description ?? patched.match(/^description:\s*(.+)$/m)?.[1].replace(/^['"]|['"]$/g, '');
|
|
184
|
+
return { success: true, title: `Skill patched: ${resultName}`, action, name: resultName, description: resultDescription, path: relativePath, summary: 'Replaced one matching block.' };
|
|
185
|
+
});
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
export const SKILL_MANAGE_GUIDANCE = `You can create and update workspace skills using the skill_manage tool.
|