@ottocode/sdk 0.1.313 → 0.1.314
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 +1 -1
- package/src/config/src/index.ts +36 -7
- package/src/config/src/manager.ts +1 -0
- package/src/config/src/paths.ts +221 -3
- package/src/core/src/tools/builtin/fs/copy-attachment.ts +28 -13
- package/src/core/src/tools/builtin/git-identity.ts +37 -3
- package/src/core/src/tools/builtin/git.ts +8 -2
- package/src/core/src/tools/builtin/shell.ts +8 -2
- package/src/core/src/tools/builtin/terminal.ts +11 -3
- package/src/index.ts +15 -0
- package/src/providers/src/catalog-manual.ts +13 -0
- package/src/providers/src/utils.ts +6 -2
- package/src/types/src/config.ts +11 -1
package/package.json
CHANGED
package/src/config/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getProjectConfigDir,
|
|
3
|
+
getProjectConfigPath,
|
|
4
|
+
getProjectStateDir,
|
|
5
|
+
getLegacyProjectDataDir,
|
|
2
6
|
getGlobalConfigPath,
|
|
3
7
|
getGlobalSkillsConfigPath,
|
|
4
|
-
getLocalDataDir,
|
|
5
8
|
ensureDir,
|
|
6
9
|
fileExists,
|
|
7
10
|
joinPath,
|
|
@@ -46,6 +49,7 @@ const DEFAULTS: {
|
|
|
46
49
|
releaseToSend: false,
|
|
47
50
|
fullWidthContent: false,
|
|
48
51
|
autoCompactThresholdTokens: null,
|
|
52
|
+
coAuthorCommits: false,
|
|
49
53
|
ottoEnabled: true,
|
|
50
54
|
},
|
|
51
55
|
providers: DEFAULT_PROVIDER_SETTINGS,
|
|
@@ -64,9 +68,21 @@ export async function loadConfig(
|
|
|
64
68
|
? String(projectRootInput)
|
|
65
69
|
: process.cwd();
|
|
66
70
|
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
71
|
+
const projectConfigDir = getProjectConfigDir(projectRoot);
|
|
72
|
+
const projectConfigPath = getProjectConfigPath(projectRoot);
|
|
73
|
+
const projectStateDir = await getProjectStateDir(projectRoot);
|
|
74
|
+
const dataDir = projectStateDir;
|
|
75
|
+
const dbPath = joinPath(projectStateDir, 'otto.sqlite');
|
|
76
|
+
const attachmentsDir = joinPath(projectStateDir, 'attachments');
|
|
77
|
+
const debugDir = joinPath(projectStateDir, 'debug');
|
|
78
|
+
const debugDumpsDir = joinPath(projectStateDir, 'debug-dumps');
|
|
79
|
+
const logsDir = joinPath(projectStateDir, 'logs');
|
|
80
|
+
const tmpDir = joinPath(projectStateDir, 'tmp');
|
|
81
|
+
const cacheDir = joinPath(projectStateDir, 'cache');
|
|
82
|
+
const legacyDbPath = joinPath(
|
|
83
|
+
getLegacyProjectDataDir(projectRoot),
|
|
84
|
+
'otto.sqlite',
|
|
85
|
+
);
|
|
70
86
|
const globalConfigPath = getGlobalConfigPath();
|
|
71
87
|
const globalSkillsConfigPath = getGlobalSkillsConfigPath();
|
|
72
88
|
|
|
@@ -81,7 +97,12 @@ export async function loadConfig(
|
|
|
81
97
|
filterProjectConfig(projectCfg),
|
|
82
98
|
);
|
|
83
99
|
|
|
84
|
-
await ensureDir(
|
|
100
|
+
await ensureDir(projectStateDir);
|
|
101
|
+
if ((await fileExists(legacyDbPath)) && !(await fileExists(dbPath))) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`Legacy Otto database found at ${legacyDbPath}. Run: otto storage migrate`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
85
106
|
|
|
86
107
|
return {
|
|
87
108
|
projectRoot,
|
|
@@ -89,11 +110,19 @@ export async function loadConfig(
|
|
|
89
110
|
providers: merged.providers as OttoConfig['providers'],
|
|
90
111
|
skills: merged.skills as OttoConfig['skills'],
|
|
91
112
|
paths: {
|
|
92
|
-
|
|
93
|
-
dbPath,
|
|
113
|
+
projectConfigDir,
|
|
94
114
|
projectConfigPath: (await fileExists(projectConfigPath))
|
|
95
115
|
? projectConfigPath
|
|
96
116
|
: null,
|
|
117
|
+
projectStateDir,
|
|
118
|
+
dataDir,
|
|
119
|
+
dbPath,
|
|
120
|
+
attachmentsDir,
|
|
121
|
+
debugDir,
|
|
122
|
+
debugDumpsDir,
|
|
123
|
+
logsDir,
|
|
124
|
+
tmpDir,
|
|
125
|
+
cacheDir,
|
|
97
126
|
globalConfigPath: (await fileExists(globalConfigPath))
|
|
98
127
|
? globalConfigPath
|
|
99
128
|
: null,
|
package/src/config/src/paths.ts
CHANGED
|
@@ -110,18 +110,236 @@ export function getSessionSystemPromptPath(sessionId: string): string {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/** Resolve the user-level Otto home directory for project state storage. */
|
|
114
|
+
export function getOttoHomeDir(): string {
|
|
115
|
+
const ottoHome = process.env.OTTO_HOME;
|
|
116
|
+
if (ottoHome?.trim()) return ottoHome.replace(/\\/g, '/');
|
|
117
|
+
if (process.platform === 'win32') {
|
|
118
|
+
const appData = (process.env.APPDATA || '').replace(/\\/g, '/');
|
|
119
|
+
const base = appData || joinPath(getHomeDir(), 'AppData', 'Roaming');
|
|
120
|
+
return joinPath(base, 'otto');
|
|
121
|
+
}
|
|
122
|
+
return joinPath(getHomeDir(), '.local', 'state', 'otto');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Resolve the root directory containing per-project state directories. */
|
|
126
|
+
export function getProjectsStateRoot(): string {
|
|
127
|
+
return joinPath(getOttoHomeDir(), 'projects');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const projectIdCache = new Map<string, Promise<string>>();
|
|
131
|
+
|
|
132
|
+
/** Build a stable readable project ID from basename and git remote/path hash. */
|
|
133
|
+
export async function getProjectId(projectRoot: string): Promise<string> {
|
|
134
|
+
const normalizedProjectRoot = projectRoot.replace(/\\/g, '/');
|
|
135
|
+
const cached = projectIdCache.get(normalizedProjectRoot);
|
|
136
|
+
if (cached) return cached;
|
|
137
|
+
|
|
138
|
+
const projectId = resolveProjectId(normalizedProjectRoot);
|
|
139
|
+
projectIdCache.set(normalizedProjectRoot, projectId);
|
|
140
|
+
return projectId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function resolveProjectId(
|
|
144
|
+
normalizedProjectRoot: string,
|
|
145
|
+
): Promise<string> {
|
|
146
|
+
const slug = sanitizeProjectSlug(getPathBasename(normalizedProjectRoot));
|
|
147
|
+
const hashInput =
|
|
148
|
+
(await readGitRemoteUrl(normalizedProjectRoot)) ??
|
|
149
|
+
(await getCanonicalProjectRoot(normalizedProjectRoot));
|
|
150
|
+
const hash = (await sha256Hex(hashInput)).slice(0, 8);
|
|
151
|
+
return `${slug}-${hash}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Resolve the repository-local Otto project config directory. */
|
|
155
|
+
export function getProjectConfigDir(projectRoot: string): string {
|
|
114
156
|
return joinPath(projectRoot, '.otto');
|
|
115
157
|
}
|
|
116
158
|
|
|
159
|
+
/** Resolve the repository-local Otto project config file path. */
|
|
160
|
+
export function getProjectConfigPath(projectRoot: string): string {
|
|
161
|
+
return joinPath(getProjectConfigDir(projectRoot), 'config.json');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Resolve the user-level state directory for a project. */
|
|
165
|
+
export async function getProjectStateDir(projectRoot: string): Promise<string> {
|
|
166
|
+
return joinPath(getProjectsStateRoot(), await getProjectId(projectRoot));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Resolve the project SQLite database path under user-level state. */
|
|
170
|
+
export async function getProjectDbPath(projectRoot: string): Promise<string> {
|
|
171
|
+
return joinPath(await getProjectStateDir(projectRoot), 'otto.sqlite');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Resolve the project attachments directory under user-level state. */
|
|
175
|
+
export async function getProjectAttachmentsDir(
|
|
176
|
+
projectRoot: string,
|
|
177
|
+
): Promise<string> {
|
|
178
|
+
return joinPath(await getProjectStateDir(projectRoot), 'attachments');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Resolve the project debug directory under user-level state. */
|
|
182
|
+
export async function getProjectDebugDir(projectRoot: string): Promise<string> {
|
|
183
|
+
return joinPath(await getProjectStateDir(projectRoot), 'debug');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Resolve the project debug dumps directory under user-level state. */
|
|
187
|
+
export async function getProjectDebugDumpsDir(
|
|
188
|
+
projectRoot: string,
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
return joinPath(await getProjectStateDir(projectRoot), 'debug-dumps');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Resolve the project logs directory under user-level state. */
|
|
194
|
+
export async function getProjectLogsDir(projectRoot: string): Promise<string> {
|
|
195
|
+
return joinPath(await getProjectStateDir(projectRoot), 'logs');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Resolve the project temporary files directory under user-level state. */
|
|
199
|
+
export async function getProjectTmpDir(projectRoot: string): Promise<string> {
|
|
200
|
+
return joinPath(await getProjectStateDir(projectRoot), 'tmp');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Resolve the project cache directory under user-level state. */
|
|
204
|
+
export async function getProjectCacheDir(projectRoot: string): Promise<string> {
|
|
205
|
+
return joinPath(await getProjectStateDir(projectRoot), 'cache');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Resolve the legacy repository-local data directory used before migration. */
|
|
209
|
+
export function getLegacyProjectDataDir(projectRoot: string): string {
|
|
210
|
+
return joinPath(projectRoot, '.otto');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @deprecated Use getLegacyProjectDataDir() for legacy project-local data. */
|
|
214
|
+
export function getLocalDataDir(projectRoot: string): string {
|
|
215
|
+
return getLegacyProjectDataDir(projectRoot);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeProjectSlug(name: string): string {
|
|
219
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, '-') || 'project';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getPathBasename(path: string): string {
|
|
223
|
+
const trimmed = path.replace(/\/+$/g, '');
|
|
224
|
+
const index = trimmed.lastIndexOf('/');
|
|
225
|
+
return index >= 0 ? trimmed.slice(index + 1) : trimmed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function sha256Hex(input: string): Promise<string> {
|
|
229
|
+
const subtle = globalThis.crypto?.subtle;
|
|
230
|
+
if (subtle) {
|
|
231
|
+
const encoded = new TextEncoder().encode(input);
|
|
232
|
+
const digest = await subtle.digest('SHA-256', encoded);
|
|
233
|
+
return Array.from(new Uint8Array(digest), (byte) =>
|
|
234
|
+
byte.toString(16).padStart(2, '0'),
|
|
235
|
+
).join('');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { createHash } = await loadCrypto();
|
|
239
|
+
return createHash('sha256').update(input).digest('hex');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function getCanonicalProjectRoot(projectRoot: string): Promise<string> {
|
|
243
|
+
try {
|
|
244
|
+
const { realpath } = await loadFsPromises();
|
|
245
|
+
return (await realpath(projectRoot)).replace(/\\/g, '/');
|
|
246
|
+
} catch {
|
|
247
|
+
return projectRoot.replace(/\\/g, '/');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function readGitRemoteUrl(
|
|
252
|
+
projectRoot: string,
|
|
253
|
+
): Promise<string | undefined> {
|
|
254
|
+
const currentBranchRemote = await readCurrentBranchRemote(projectRoot);
|
|
255
|
+
const remoteNames = currentBranchRemote
|
|
256
|
+
? [currentBranchRemote, 'origin']
|
|
257
|
+
: ['origin'];
|
|
258
|
+
|
|
259
|
+
for (const remoteName of remoteNames) {
|
|
260
|
+
const remoteUrl = await readGitConfigValue(
|
|
261
|
+
projectRoot,
|
|
262
|
+
`remote.${remoteName}.url`,
|
|
263
|
+
);
|
|
264
|
+
if (remoteUrl) return remoteUrl;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const remoteList = await readGitConfigValues(
|
|
268
|
+
projectRoot,
|
|
269
|
+
'config',
|
|
270
|
+
'--get-regexp',
|
|
271
|
+
'remote\\..*\\.url',
|
|
272
|
+
);
|
|
273
|
+
return remoteList
|
|
274
|
+
.map((line) => line.trim().split(/\s+/, 2)[1])
|
|
275
|
+
.find((url) => url?.trim());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function readCurrentBranchRemote(
|
|
279
|
+
projectRoot: string,
|
|
280
|
+
): Promise<string | undefined> {
|
|
281
|
+
const branch = await readGitConfigValues(
|
|
282
|
+
projectRoot,
|
|
283
|
+
'rev-parse',
|
|
284
|
+
'--abbrev-ref',
|
|
285
|
+
'HEAD',
|
|
286
|
+
);
|
|
287
|
+
const branchName = branch[0];
|
|
288
|
+
if (!branchName || branchName === 'HEAD') return undefined;
|
|
289
|
+
return readGitConfigValue(projectRoot, `branch.${branchName}.remote`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function readGitConfigValue(
|
|
293
|
+
projectRoot: string,
|
|
294
|
+
key: string,
|
|
295
|
+
): Promise<string | undefined> {
|
|
296
|
+
const values = await readGitConfigValues(projectRoot, 'config', '--get', key);
|
|
297
|
+
return values[0];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function readGitConfigValues(
|
|
301
|
+
projectRoot: string,
|
|
302
|
+
...args: string[]
|
|
303
|
+
): Promise<string[]> {
|
|
304
|
+
try {
|
|
305
|
+
const { execFile } = await loadChildProcess();
|
|
306
|
+
const { promisify } = await loadUtil();
|
|
307
|
+
const execFileAsync = promisify(execFile);
|
|
308
|
+
const { stdout } = await execFileAsync('git', ['-C', projectRoot, ...args]);
|
|
309
|
+
const output = String(stdout);
|
|
310
|
+
return output
|
|
311
|
+
.split('\n')
|
|
312
|
+
.map((line) => line.trim())
|
|
313
|
+
.filter(Boolean);
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
117
319
|
async function loadFsPromises(): Promise<typeof import('node:fs/promises')> {
|
|
118
320
|
return Function('specifier', 'return import(specifier)')('node:fs/promises');
|
|
119
321
|
}
|
|
120
322
|
|
|
323
|
+
async function loadChildProcess(): Promise<
|
|
324
|
+
typeof import('node:child_process')
|
|
325
|
+
> {
|
|
326
|
+
return Function(
|
|
327
|
+
'specifier',
|
|
328
|
+
'return import(specifier)',
|
|
329
|
+
)('node:child_process');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function loadUtil(): Promise<typeof import('node:util')> {
|
|
333
|
+
return Function('specifier', 'return import(specifier)')('node:util');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function loadCrypto(): Promise<typeof import('node:crypto')> {
|
|
337
|
+
return Function('specifier', 'return import(specifier)')('node:crypto');
|
|
338
|
+
}
|
|
339
|
+
|
|
121
340
|
export async function ensureDir(dir: string) {
|
|
122
|
-
const { mkdir
|
|
341
|
+
const { mkdir } = await loadFsPromises();
|
|
123
342
|
await mkdir(dir, { recursive: true }).catch(() => {});
|
|
124
|
-
await writeFile(joinPath(dir, '.keep'), '').catch(() => {});
|
|
125
343
|
}
|
|
126
344
|
|
|
127
345
|
export async function fileExists(p: string) {
|
|
@@ -3,6 +3,10 @@ import { copyFile, mkdir, readFile, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, extname, join } from 'node:path';
|
|
4
4
|
import { tool, type Tool } from 'ai';
|
|
5
5
|
import { z } from 'zod/v3';
|
|
6
|
+
import {
|
|
7
|
+
loadConfig,
|
|
8
|
+
type OttoConfig,
|
|
9
|
+
} from '../../../../../config/src/index.ts';
|
|
6
10
|
import { createToolError, type ToolResponse } from '../../error.ts';
|
|
7
11
|
import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
|
|
8
12
|
import { rememberFileWrite } from './read-tracker.ts';
|
|
@@ -16,10 +20,11 @@ type AttachmentMetadata = {
|
|
|
16
20
|
sha256: string;
|
|
17
21
|
kind: 'image' | 'pdf' | 'text' | 'binary';
|
|
18
22
|
originalPath: string;
|
|
23
|
+
storageRoot?: 'project-state';
|
|
24
|
+
relativePath?: string;
|
|
19
25
|
createdAt: string;
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
const ATTACHMENTS_DIR = '.otto/attachments';
|
|
23
28
|
const MIME_EXTENSIONS: Record<string, string[]> = {
|
|
24
29
|
'image/png': ['.png'],
|
|
25
30
|
'image/jpeg': ['.jpg', '.jpeg'],
|
|
@@ -43,12 +48,11 @@ function replaceExtension(path: string, extension: string): string {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
async function readAttachmentMetadata(
|
|
46
|
-
|
|
51
|
+
cfg: OttoConfig,
|
|
47
52
|
attachmentId: string,
|
|
48
53
|
): Promise<AttachmentMetadata> {
|
|
49
54
|
const metadataPath = join(
|
|
50
|
-
|
|
51
|
-
ATTACHMENTS_DIR,
|
|
55
|
+
cfg.paths.attachmentsDir,
|
|
52
56
|
attachmentId,
|
|
53
57
|
'metadata.json',
|
|
54
58
|
);
|
|
@@ -56,6 +60,19 @@ async function readAttachmentMetadata(
|
|
|
56
60
|
return JSON.parse(raw) as AttachmentMetadata;
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function resolveAttachmentSource(
|
|
64
|
+
cfg: OttoConfig,
|
|
65
|
+
metadata: AttachmentMetadata,
|
|
66
|
+
): string {
|
|
67
|
+
if (metadata.storageRoot === 'project-state' && metadata.relativePath) {
|
|
68
|
+
return join(cfg.paths.projectStateDir, metadata.relativePath);
|
|
69
|
+
}
|
|
70
|
+
return join(
|
|
71
|
+
cfg.paths.projectStateDir,
|
|
72
|
+
metadata.relativePath ?? metadata.originalPath,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
export function buildCopyAttachmentTool(projectRoot: string): {
|
|
60
77
|
name: string;
|
|
61
78
|
tool: Tool;
|
|
@@ -83,8 +100,8 @@ export function buildCopyAttachmentTool(projectRoot: string): {
|
|
|
83
100
|
async execute({
|
|
84
101
|
attachmentId,
|
|
85
102
|
targetPath,
|
|
86
|
-
overwrite,
|
|
87
|
-
createDirs,
|
|
103
|
+
overwrite = false,
|
|
104
|
+
createDirs = true,
|
|
88
105
|
}: {
|
|
89
106
|
attachmentId: string;
|
|
90
107
|
targetPath: string;
|
|
@@ -132,11 +149,9 @@ export function buildCopyAttachmentTool(projectRoot: string): {
|
|
|
132
149
|
}
|
|
133
150
|
|
|
134
151
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
139
|
-
const source = join(projectRoot, metadata.originalPath);
|
|
152
|
+
const cfg = await loadConfig(projectRoot);
|
|
153
|
+
const metadata = await readAttachmentMetadata(cfg, attachmentId);
|
|
154
|
+
const source = resolveAttachmentSource(cfg, metadata);
|
|
140
155
|
const expectedExtensions = getExpectedExtensions(metadata.mimeType);
|
|
141
156
|
const targetExtension = extname(requestedPath).toLowerCase();
|
|
142
157
|
const preferredExtension = expectedExtensions[0];
|
|
@@ -145,7 +160,7 @@ export function buildCopyAttachmentTool(projectRoot: string): {
|
|
|
145
160
|
? replaceExtension(requestedPath, preferredExtension)
|
|
146
161
|
: requestedPath;
|
|
147
162
|
const extensionAdjusted = req !== requestedPath;
|
|
148
|
-
const target = resolveSafePath(projectRoot, req);
|
|
163
|
+
const target = resolveSafePath(cfg.projectRoot, req);
|
|
149
164
|
try {
|
|
150
165
|
await stat(target);
|
|
151
166
|
if (!overwrite) {
|
|
@@ -168,7 +183,7 @@ export function buildCopyAttachmentTool(projectRoot: string): {
|
|
|
168
183
|
await mkdir(dirname(target), { recursive: true });
|
|
169
184
|
}
|
|
170
185
|
await copyFile(source, target);
|
|
171
|
-
await rememberFileWrite(projectRoot, target);
|
|
186
|
+
await rememberFileWrite(cfg.projectRoot, target);
|
|
172
187
|
const bytes = await readFile(target);
|
|
173
188
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
174
189
|
return {
|
|
@@ -1,10 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { getGlobalConfigPath } from '../../../../config/src/paths.ts';
|
|
3
|
+
|
|
1
4
|
const OTTOCODE_BOT_USER_ID = '261994719';
|
|
2
5
|
|
|
3
6
|
export const OTTOCODE_BOT_NAME = 'ottocode-io[bot]';
|
|
4
7
|
export const OTTOCODE_BOT_EMAIL = `${OTTOCODE_BOT_USER_ID}+${OTTOCODE_BOT_NAME}@users.noreply.github.com`;
|
|
5
8
|
export const OTTOCODE_CO_AUTHOR = `Co-authored-by: ${OTTOCODE_BOT_NAME} <${OTTOCODE_BOT_EMAIL}>`;
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
type JsonObject = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
function readCoAuthorCommits(filePath: string): boolean | undefined {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
15
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const defaults = (parsed as JsonObject).defaults;
|
|
19
|
+
if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const value = (defaults as JsonObject).coAuthorCommits;
|
|
23
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
24
|
+
} catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function shouldCoAuthorCommits(_projectRoot?: string): boolean {
|
|
30
|
+
return readCoAuthorCommits(getGlobalConfigPath()) ?? false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function appendCoAuthorTrailer(
|
|
34
|
+
message: string,
|
|
35
|
+
enabled = false,
|
|
36
|
+
): string {
|
|
37
|
+
if (!enabled) return message;
|
|
8
38
|
if (message.includes(OTTOCODE_CO_AUTHOR)) return message;
|
|
9
39
|
return `${message}\n\n${OTTOCODE_CO_AUTHOR}`;
|
|
10
40
|
}
|
|
@@ -12,9 +42,13 @@ export function appendCoAuthorTrailer(message: string): string {
|
|
|
12
42
|
const GIT_COMMIT_MSG_RE =
|
|
13
43
|
/git\s+commit\s+(?:[^"']*?)(?:-[a-z]*m|-m)\s+(["'])([\s\S]*?)\1/g;
|
|
14
44
|
|
|
15
|
-
export function injectCoAuthorIntoGitCommit(
|
|
45
|
+
export function injectCoAuthorIntoGitCommit(
|
|
46
|
+
cmd: string,
|
|
47
|
+
enabled = false,
|
|
48
|
+
): string {
|
|
49
|
+
if (!enabled) return cmd;
|
|
16
50
|
return cmd.replace(GIT_COMMIT_MSG_RE, (match, quote, msg) => {
|
|
17
|
-
const patched = appendCoAuthorTrailer(msg);
|
|
51
|
+
const patched = appendCoAuthorTrailer(msg, true);
|
|
18
52
|
return match.replace(
|
|
19
53
|
`${quote}${msg}${quote}`,
|
|
20
54
|
`${quote}${patched}${quote}`,
|
|
@@ -6,7 +6,10 @@ import GIT_STATUS_DESCRIPTION from './git.status.txt' with { type: 'text' };
|
|
|
6
6
|
import GIT_DIFF_DESCRIPTION from './git.diff.txt' with { type: 'text' };
|
|
7
7
|
import GIT_COMMIT_DESCRIPTION from './git.commit.txt' with { type: 'text' };
|
|
8
8
|
import { createToolError, type ToolResponse } from '../error.ts';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
appendCoAuthorTrailer,
|
|
11
|
+
shouldCoAuthorCommits,
|
|
12
|
+
} from './git-identity.ts';
|
|
10
13
|
|
|
11
14
|
const execAsync = promisify(exec);
|
|
12
15
|
|
|
@@ -120,7 +123,10 @@ export function buildGitTools(
|
|
|
120
123
|
});
|
|
121
124
|
}
|
|
122
125
|
const gitRoot = await findGitRoot();
|
|
123
|
-
const fullMessage = appendCoAuthorTrailer(
|
|
126
|
+
const fullMessage = appendCoAuthorTrailer(
|
|
127
|
+
message,
|
|
128
|
+
shouldCoAuthorCommits(projectRoot),
|
|
129
|
+
);
|
|
124
130
|
const args = [
|
|
125
131
|
'git',
|
|
126
132
|
'-C',
|
|
@@ -5,7 +5,10 @@ import { z } from 'zod/v3';
|
|
|
5
5
|
import DESCRIPTION from './shell.txt' with { type: 'text' };
|
|
6
6
|
import { getShellExecutionConfig } from '../bin-manager.ts';
|
|
7
7
|
import { createToolError, type ToolResponse } from '../error.ts';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
injectCoAuthorIntoGitCommit,
|
|
10
|
+
shouldCoAuthorCommits,
|
|
11
|
+
} from './git-identity.ts';
|
|
9
12
|
|
|
10
13
|
function normalizePath(p: string) {
|
|
11
14
|
const normalized = p.replace(/\\/g, '/');
|
|
@@ -248,7 +251,10 @@ export function buildShellTool(projectRoot: string): {
|
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
const absCwd = resolveSafePath(projectRoot, cwd || '.');
|
|
251
|
-
const finalCmd = injectCoAuthorIntoGitCommit(
|
|
254
|
+
const finalCmd = injectCoAuthorIntoGitCommit(
|
|
255
|
+
cmd,
|
|
256
|
+
shouldCoAuthorCommits(projectRoot),
|
|
257
|
+
);
|
|
252
258
|
const shellExecutor = shellExecutorContext.getStore();
|
|
253
259
|
if (shellExecutor) {
|
|
254
260
|
return shellExecutor(
|
|
@@ -5,7 +5,10 @@ import { createToolError } from '../error.ts';
|
|
|
5
5
|
import type { TerminalManager } from '../../terminals/index.ts';
|
|
6
6
|
import type { TerminalStatus } from '../../terminals/terminal.ts';
|
|
7
7
|
import { normalizeTerminalLine } from '../../utils/ansi.ts';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
injectCoAuthorIntoGitCommit,
|
|
10
|
+
shouldCoAuthorCommits,
|
|
11
|
+
} from './git-identity.ts';
|
|
9
12
|
|
|
10
13
|
function shellQuote(segment: string): string {
|
|
11
14
|
if (/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
@@ -108,6 +111,7 @@ export function buildTerminalTool(
|
|
|
108
111
|
execute: async (params) => {
|
|
109
112
|
try {
|
|
110
113
|
const { operation } = params;
|
|
114
|
+
const coAuthorCommits = shouldCoAuthorCommits(projectRoot);
|
|
111
115
|
|
|
112
116
|
switch (operation) {
|
|
113
117
|
case 'start': {
|
|
@@ -169,7 +173,9 @@ export function buildTerminalTool(
|
|
|
169
173
|
|
|
170
174
|
if (initialCommand) {
|
|
171
175
|
queueMicrotask(() => {
|
|
172
|
-
term.write(
|
|
176
|
+
term.write(
|
|
177
|
+
`${injectCoAuthorIntoGitCommit(initialCommand, coAuthorCommits)}\n`,
|
|
178
|
+
);
|
|
173
179
|
});
|
|
174
180
|
}
|
|
175
181
|
|
|
@@ -243,7 +249,9 @@ export function buildTerminalTool(
|
|
|
243
249
|
return createToolError(`Terminal ${params.terminalId} not found`);
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
term.write(
|
|
252
|
+
term.write(
|
|
253
|
+
injectCoAuthorIntoGitCommit(params.input, coAuthorCommits),
|
|
254
|
+
);
|
|
247
255
|
|
|
248
256
|
return {
|
|
249
257
|
ok: true,
|
package/src/index.ts
CHANGED
|
@@ -253,6 +253,20 @@ export type {
|
|
|
253
253
|
export { loadConfig, read as readConfig } from './config/src/index.ts';
|
|
254
254
|
export {
|
|
255
255
|
getLocalDataDir,
|
|
256
|
+
getLegacyProjectDataDir,
|
|
257
|
+
getOttoHomeDir,
|
|
258
|
+
getProjectsStateRoot,
|
|
259
|
+
getProjectId,
|
|
260
|
+
getProjectConfigDir,
|
|
261
|
+
getProjectConfigPath,
|
|
262
|
+
getProjectStateDir,
|
|
263
|
+
getProjectDbPath,
|
|
264
|
+
getProjectAttachmentsDir,
|
|
265
|
+
getProjectDebugDir,
|
|
266
|
+
getProjectDebugDumpsDir,
|
|
267
|
+
getProjectLogsDir,
|
|
268
|
+
getProjectTmpDir,
|
|
269
|
+
getProjectCacheDir,
|
|
256
270
|
getGlobalConfigDir,
|
|
257
271
|
getGlobalConfigPath,
|
|
258
272
|
getGlobalSkillsConfigPath,
|
|
@@ -334,6 +348,7 @@ export {
|
|
|
334
348
|
export {
|
|
335
349
|
appendCoAuthorTrailer,
|
|
336
350
|
injectCoAuthorIntoGitCommit,
|
|
351
|
+
shouldCoAuthorCommits,
|
|
337
352
|
OTTOCODE_BOT_NAME,
|
|
338
353
|
OTTOCODE_BOT_EMAIL,
|
|
339
354
|
OTTOCODE_CO_AUTHOR,
|
|
@@ -154,6 +154,18 @@ export function appendXaiGrokCliModels<T extends { models: ModelInfo[] }>(
|
|
|
154
154
|
return { ...entry, models: [...mergedModels, ...missingModels] };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
const DEPRECATED_KIMI_MODEL_IDS = new Set([
|
|
158
|
+
'kimi-k2-0711-preview',
|
|
159
|
+
'kimi-k2-0905-preview',
|
|
160
|
+
'kimi-k2-thinking',
|
|
161
|
+
'kimi-k2-thinking-turbo',
|
|
162
|
+
'kimi-k2-turbo-preview',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
export function filterAvailableKimiModels(models: ModelInfo[]): ModelInfo[] {
|
|
166
|
+
return models.filter((model) => !DEPRECATED_KIMI_MODEL_IDS.has(model.id));
|
|
167
|
+
}
|
|
168
|
+
|
|
157
169
|
export function applyOfficialKimiCatalogMetadata<
|
|
158
170
|
T extends ProviderCatalogEntry,
|
|
159
171
|
>(entry: T | undefined): T | undefined {
|
|
@@ -163,6 +175,7 @@ export function applyOfficialKimiCatalogMetadata<
|
|
|
163
175
|
);
|
|
164
176
|
return {
|
|
165
177
|
...entry,
|
|
178
|
+
models: filterAvailableKimiModels(entry.models),
|
|
166
179
|
label: entry.label === 'Moonshot AI' ? 'Kimi' : entry.label,
|
|
167
180
|
env,
|
|
168
181
|
doc: 'https://platform.kimi.ai/docs/api/overview.md',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { catalog } from './catalog-merged.ts';
|
|
2
|
+
import { filterAvailableKimiModels } from './catalog-manual.ts';
|
|
2
3
|
import { getCachedProviderCatalogEntry } from './model-catalog-cache.ts';
|
|
3
4
|
import { mergeModelLists } from './model-merge.ts';
|
|
4
5
|
import type {
|
|
@@ -45,7 +46,7 @@ const PREFERRED_FAST_MODELS: Partial<Record<ProviderId, string[]>> = {
|
|
|
45
46
|
xai: ['grok-code-fast-1', 'grok-4-fast'],
|
|
46
47
|
zai: ['glm-4.5-flash'],
|
|
47
48
|
copilot: ['gpt-4.1-mini'],
|
|
48
|
-
moonshot: ['kimi-k2-
|
|
49
|
+
moonshot: ['kimi-k2.7-code'],
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
const PREFERRED_FAST_MODELS_OAUTH: Partial<Record<ProviderId, string[]>> = {
|
|
@@ -263,7 +264,10 @@ function getProviderModels(provider: ProviderId): ModelInfo[] {
|
|
|
263
264
|
const cachedModels = getCachedProviderCatalogEntry(
|
|
264
265
|
catalogProvider ?? provider,
|
|
265
266
|
)?.models;
|
|
266
|
-
|
|
267
|
+
const models = mergeModelLists(catalogModels, cachedModels);
|
|
268
|
+
return catalogProvider === 'moonshot'
|
|
269
|
+
? filterAvailableKimiModels(models)
|
|
270
|
+
: models;
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
export function modelSupportsReasoning(
|
package/src/types/src/config.ts
CHANGED
|
@@ -38,6 +38,8 @@ export type DefaultConfig = {
|
|
|
38
38
|
releaseToSend?: boolean;
|
|
39
39
|
fullWidthContent?: boolean;
|
|
40
40
|
autoCompactThresholdTokens?: number | null;
|
|
41
|
+
/** Adds the ottocode bot as a co-author on commits made through Otto. */
|
|
42
|
+
coAuthorCommits?: boolean;
|
|
41
43
|
/** Enables the otto supervisor agent and persistent goals (disabled together). */
|
|
42
44
|
ottoEnabled?: boolean;
|
|
43
45
|
};
|
|
@@ -74,9 +76,17 @@ export type SkillSettings = {
|
|
|
74
76
|
* Path configuration
|
|
75
77
|
*/
|
|
76
78
|
export type PathConfig = {
|
|
79
|
+
projectConfigDir: string;
|
|
80
|
+
projectConfigPath: string | null;
|
|
81
|
+
projectStateDir: string;
|
|
77
82
|
dataDir: string;
|
|
78
83
|
dbPath: string;
|
|
79
|
-
|
|
84
|
+
attachmentsDir: string;
|
|
85
|
+
debugDir: string;
|
|
86
|
+
debugDumpsDir: string;
|
|
87
|
+
logsDir: string;
|
|
88
|
+
tmpDir: string;
|
|
89
|
+
cacheDir: string;
|
|
80
90
|
globalConfigPath: string | null;
|
|
81
91
|
};
|
|
82
92
|
|