@openturtle/cli 0.3.2 → 0.3.4
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 +52 -152
- package/RELEASE_RUNBOOK.md +193 -0
- package/dist/commands/collaboration.js +88 -4
- package/dist/commands/local-knowledge.js +129 -0
- package/dist/commands/resources.js +2 -5
- package/dist/core/auth.js +4 -2
- package/dist/core/auto-update.js +30 -2
- package/dist/core/installations.js +41 -0
- package/dist/core/{mcp-config.js → legacy-agent-cleanup.js} +10 -42
- package/dist/core/store.js +1 -20
- package/dist/index.js +39 -85
- package/dist/installers/agents.js +79 -222
- package/dist/installers/legacy-git-cleanup.js +29 -0
- package/dist/local-knowledge/index.js +398 -0
- package/dist/local-knowledge/library.js +328 -0
- package/dist/local-knowledge/root.js +5 -0
- package/package.json +5 -3
- package/skill/openturtle-cli/SKILL.md +81 -0
- package/dist/commands/hook.js +0 -162
- package/dist/installers/git.js +0 -63
- package/dist/mcp/server.js +0 -185
- package/dist/watchers/importers.js +0 -69
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readJsonBody } from '../core/input.js';
|
|
2
|
+
import { createLocalKnowledgeEntry, getLocalKnowledgeAsset, importLocalKnowledgePaths, listLocalKnowledgeAssets, removeLocalKnowledgeAsset, updateLocalKnowledgeEntry, } from '../local-knowledge/library.js';
|
|
3
|
+
import { readLocalKnowledgeIndexedContent, searchLocalKnowledgeIndex, } from '../local-knowledge/index.js';
|
|
4
|
+
import { localKnowledgeStoreRoot } from '../local-knowledge/root.js';
|
|
5
|
+
export function createLocalKnowledgeCommands(root = localKnowledgeStoreRoot()) {
|
|
6
|
+
return {
|
|
7
|
+
root,
|
|
8
|
+
list: () => listLocalKnowledgeAssets(root),
|
|
9
|
+
async search(query, limit = 20, matchMode = 'any') {
|
|
10
|
+
const assets = await listLocalKnowledgeAssets(root);
|
|
11
|
+
const hits = await searchLocalKnowledgeIndex(assets, query, Math.max(1, Math.min(limit, 100)), root, matchMode);
|
|
12
|
+
const byId = new Map(assets.map((asset) => [asset.id, asset]));
|
|
13
|
+
const items = hits.flatMap((hit) => {
|
|
14
|
+
const asset = byId.get(hit.assetId);
|
|
15
|
+
return asset ? [{ ...asset, matches: hit.matches }] : [];
|
|
16
|
+
});
|
|
17
|
+
return { root, items, count: items.length };
|
|
18
|
+
},
|
|
19
|
+
async read(assetId, truncate = 200_000) {
|
|
20
|
+
const asset = getLocalKnowledgeAsset(assetId, root);
|
|
21
|
+
if (!asset)
|
|
22
|
+
throw new Error('Local knowledge asset was not found');
|
|
23
|
+
const indexed = await readLocalKnowledgeIndexedContent(asset, Math.max(1, Math.min(truncate, 1_000_000)), root);
|
|
24
|
+
return {
|
|
25
|
+
...asset,
|
|
26
|
+
text: indexed.content,
|
|
27
|
+
content_available: indexed.contentAvailable,
|
|
28
|
+
truncated: indexed.truncated,
|
|
29
|
+
index_status: indexed.indexStatus,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
importPaths(paths, dryRun = false) {
|
|
33
|
+
if (dryRun)
|
|
34
|
+
return Promise.resolve(localDryRun(root, 'import', { paths }));
|
|
35
|
+
return importLocalKnowledgePaths(paths, root);
|
|
36
|
+
},
|
|
37
|
+
create(input, dryRun = false) {
|
|
38
|
+
if (dryRun)
|
|
39
|
+
return Promise.resolve(localDryRun(root, 'create', { input }));
|
|
40
|
+
return createLocalKnowledgeEntry(input, root);
|
|
41
|
+
},
|
|
42
|
+
update(assetId, input, dryRun = false) {
|
|
43
|
+
if (dryRun)
|
|
44
|
+
return Promise.resolve(localDryRun(root, 'update', { asset_id: assetId, input }));
|
|
45
|
+
return updateLocalKnowledgeEntry(assetId, input, root);
|
|
46
|
+
},
|
|
47
|
+
remove(assetId, dryRun = false) {
|
|
48
|
+
if (dryRun)
|
|
49
|
+
return Promise.resolve(localDryRun(root, 'remove', { asset_id: assetId }));
|
|
50
|
+
return removeLocalKnowledgeAsset(assetId, root);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function searchKnowledgeEverywhere(query, options, fetchCloud, root = localKnowledgeStoreRoot()) {
|
|
55
|
+
if (options.cloudOnly && options.localOnly)
|
|
56
|
+
throw new Error('Use --cloud-only or --local-only, not both');
|
|
57
|
+
const local = createLocalKnowledgeCommands(root);
|
|
58
|
+
const [cloudResult, localResult] = await Promise.all([
|
|
59
|
+
options.localOnly ? Promise.resolve(null) : fetchCloud(),
|
|
60
|
+
options.cloudOnly
|
|
61
|
+
? Promise.resolve(null)
|
|
62
|
+
: local.search(query, Number(options.limit) || 20, normalizeMatchMode(options.match)),
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
query,
|
|
66
|
+
cloud: cloudResult,
|
|
67
|
+
local: localResult,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function registerLocalKnowledgeCommands(knowledge, callbacks) {
|
|
71
|
+
const commands = createLocalKnowledgeCommands();
|
|
72
|
+
const local = knowledge.command('local').description('Local indexed knowledge available without Desktop');
|
|
73
|
+
const action = (fn) => (...args) => {
|
|
74
|
+
Promise.resolve(fn(...args)).catch(callbacks.fatal);
|
|
75
|
+
};
|
|
76
|
+
local.command('list').action(action(async () => callbacks.print(await commands.list())));
|
|
77
|
+
local
|
|
78
|
+
.command('search <query>')
|
|
79
|
+
.option('--limit <number>', 'maximum results', Number, 20)
|
|
80
|
+
.option('--match <mode>', 'any|all|exact', 'any')
|
|
81
|
+
.action(action(async (query, options) => callbacks.print(await commands.search(query, options.limit, normalizeMatchMode(options.match)))));
|
|
82
|
+
local
|
|
83
|
+
.command('read <assetId>')
|
|
84
|
+
.option('--truncate <characters>', 'maximum extracted characters', Number, 200_000)
|
|
85
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.read(assetId, options.truncate))));
|
|
86
|
+
local
|
|
87
|
+
.command('import <paths...>')
|
|
88
|
+
.option('--dry-run')
|
|
89
|
+
.action(action(async (paths, options) => callbacks.print(await commands.importPaths(paths, Boolean(options.dryRun)))));
|
|
90
|
+
local
|
|
91
|
+
.command('create')
|
|
92
|
+
.requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
|
|
93
|
+
.option('--dry-run')
|
|
94
|
+
.action(action(async (options) => callbacks.print(await commands.create(asEntryInput(await readJsonBody(options.bodyFile)), Boolean(options.dryRun)))));
|
|
95
|
+
local
|
|
96
|
+
.command('update <assetId>')
|
|
97
|
+
.requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
|
|
98
|
+
.option('--dry-run')
|
|
99
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.update(assetId, asEntryInput(await readJsonBody(options.bodyFile)), Boolean(options.dryRun)))));
|
|
100
|
+
local
|
|
101
|
+
.command('remove <assetId>')
|
|
102
|
+
.option('--dry-run')
|
|
103
|
+
.action(action(async (assetId, options) => callbacks.print(await commands.remove(assetId, Boolean(options.dryRun)))));
|
|
104
|
+
}
|
|
105
|
+
function normalizeMatchMode(value) {
|
|
106
|
+
return value === 'all' || value === 'exact' ? value : 'any';
|
|
107
|
+
}
|
|
108
|
+
function asEntryInput(value) {
|
|
109
|
+
const kind = value.kind;
|
|
110
|
+
if (kind !== 'note' && kind !== 'decision' && kind !== 'fact' && kind !== 'meeting') {
|
|
111
|
+
throw new Error('Local knowledge kind must be note, decision, fact, or meeting');
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
title: String(value.title || ''),
|
|
115
|
+
body: String(value.body || ''),
|
|
116
|
+
kind,
|
|
117
|
+
projectId: value.projectId == null ? undefined : String(value.projectId),
|
|
118
|
+
status: String(value.status || 'draft'),
|
|
119
|
+
attachments: Array.isArray(value.attachments)
|
|
120
|
+
? value.attachments.map((item) => {
|
|
121
|
+
const attachment = item;
|
|
122
|
+
return { name: String(attachment.name || ''), path: String(attachment.path || '') };
|
|
123
|
+
})
|
|
124
|
+
: undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function localDryRun(root, operation, input) {
|
|
128
|
+
return { dry_run: true, local_knowledge: { root, operation, ...input } };
|
|
129
|
+
}
|
|
@@ -103,16 +103,13 @@ export function doctor(workspace, target) {
|
|
|
103
103
|
project_dir: store.dir,
|
|
104
104
|
project_dir_exists: projectDirExists,
|
|
105
105
|
global_dir: path.join(process.env.HOME || '', '.openturtle', 'cli'),
|
|
106
|
-
|
|
106
|
+
integration: 'cli-skill',
|
|
107
107
|
},
|
|
108
108
|
auth: {
|
|
109
109
|
server: auth.server || null,
|
|
110
110
|
token_configured: Boolean(auth.token),
|
|
111
111
|
},
|
|
112
|
-
coexistence: {
|
|
113
|
-
avoids_project_dot_openturtle: true,
|
|
114
|
-
desktop_mcp_names_reserved: ['openturtle-knowledge', 'openturtle-automations', 'openturtle-todo'],
|
|
115
|
-
},
|
|
112
|
+
coexistence: { avoids_project_dot_openturtle: true },
|
|
116
113
|
target: target || 'all',
|
|
117
114
|
};
|
|
118
115
|
}
|
package/dist/core/auth.js
CHANGED
|
@@ -7,13 +7,15 @@ export function authPath(homeDir = os.homedir()) {
|
|
|
7
7
|
return path.join(cliGlobalDir(homeDir), 'auth.json');
|
|
8
8
|
}
|
|
9
9
|
export function readAuth(homeDir = os.homedir()) {
|
|
10
|
-
const envToken = process.env.OPENTURTLE_TOKEN;
|
|
11
|
-
const envServer = process.env.OPENTURTLE_SERVER_URL || process.env.SERVER_URL;
|
|
10
|
+
const envToken = process.env.OPENTURTLE_API_TOKEN || process.env.OPENTURTLE_TOKEN;
|
|
11
|
+
const envServer = process.env.OPENTURTLE_API_BASE_URL || process.env.OPENTURTLE_SERVER_URL || process.env.SERVER_URL;
|
|
12
|
+
const envTeamId = process.env.OPENTURTLE_TEAM_ID;
|
|
12
13
|
const config = readJsonFile(authPath(homeDir), {});
|
|
13
14
|
return {
|
|
14
15
|
...config,
|
|
15
16
|
...(envServer ? { server: envServer } : {}),
|
|
16
17
|
...(envToken ? { token: envToken } : {}),
|
|
18
|
+
...(envTeamId ? { teamId: envTeamId } : {}),
|
|
17
19
|
};
|
|
18
20
|
}
|
|
19
21
|
export function writeAuth(config, homeDir = os.homedir()) {
|
package/dist/core/auto-update.js
CHANGED
|
@@ -40,6 +40,15 @@ export function npmUpdateArgs() {
|
|
|
40
40
|
];
|
|
41
41
|
}
|
|
42
42
|
export async function updateCli(currentVersion, options = {}, dependencies = {}) {
|
|
43
|
+
if ((dependencies.env || process.env).OPENTURTLE_CLI_DISTRIBUTION === 'desktop') {
|
|
44
|
+
return {
|
|
45
|
+
current: currentVersion,
|
|
46
|
+
latest: null,
|
|
47
|
+
available: false,
|
|
48
|
+
updated: false,
|
|
49
|
+
reason: 'desktop_managed',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
43
52
|
const latest = await fetchLatestVersion(dependencies.fetchImpl);
|
|
44
53
|
if (!latest) {
|
|
45
54
|
return { current: currentVersion, latest: null, available: false, updated: false, reason: 'not_published' };
|
|
@@ -64,13 +73,16 @@ export async function updateCli(currentVersion, options = {}, dependencies = {})
|
|
|
64
73
|
};
|
|
65
74
|
}
|
|
66
75
|
await (dependencies.runNpmInstall || runNpmInstall)(npmUpdateArgs());
|
|
67
|
-
|
|
76
|
+
await (dependencies.refreshInstalledSkills || refreshInstalledSkills)();
|
|
77
|
+
return { current: currentVersion, latest, available: true, updated: true, skills_refreshed: true };
|
|
68
78
|
}
|
|
69
79
|
export function scheduleAutomaticUpdate(currentVersion, dependencies = {}) {
|
|
70
80
|
try {
|
|
71
81
|
const env = dependencies.env || process.env;
|
|
72
82
|
const argv = dependencies.argv || process.argv;
|
|
73
|
-
if (env.
|
|
83
|
+
if (env.OPENTURTLE_CLI_DISTRIBUTION === 'desktop' ||
|
|
84
|
+
env.OPENTURTLE_DISABLE_AUTO_UPDATE === '1' ||
|
|
85
|
+
env.OPENTURTLE_AUTO_UPDATE_WORKER === '1')
|
|
74
86
|
return false;
|
|
75
87
|
if (argv[2] === 'update' || !isManagedNpmInstall(dependencies.packageRoot))
|
|
76
88
|
return false;
|
|
@@ -166,6 +178,22 @@ function runNpmInstall(args) {
|
|
|
166
178
|
});
|
|
167
179
|
});
|
|
168
180
|
}
|
|
181
|
+
function refreshInstalledSkills() {
|
|
182
|
+
const entryPath = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const child = spawn(process.execPath, [entryPath, '_refresh-installs'], {
|
|
185
|
+
env: { ...process.env, OPENTURTLE_AUTO_UPDATE_WORKER: '1' },
|
|
186
|
+
stdio: 'ignore',
|
|
187
|
+
});
|
|
188
|
+
child.once('error', reject);
|
|
189
|
+
child.once('exit', (code, signal) => {
|
|
190
|
+
if (code === 0)
|
|
191
|
+
resolve();
|
|
192
|
+
else
|
|
193
|
+
reject(new Error(`Skill refresh failed (${signal || `exit ${code ?? 'unknown'}`})`));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
169
197
|
function spawnUpdateWorker(workerPath, env) {
|
|
170
198
|
const child = spawn(process.execPath, [workerPath], {
|
|
171
199
|
detached: true,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cliGlobalDir, ensureFileDir } from './paths.js';
|
|
4
|
+
export function installationsPath(homeDir) {
|
|
5
|
+
return path.join(cliGlobalDir(homeDir), 'installations.json');
|
|
6
|
+
}
|
|
7
|
+
export function readInstallations(filePath = installationsPath()) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
10
|
+
return Array.isArray(parsed.installations) ? parsed.installations : [];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function recordInstallation(installation, filePath = installationsPath()) {
|
|
17
|
+
const normalized = normalizeInstallation(installation);
|
|
18
|
+
const installations = readInstallations(filePath).filter((item) => installationKey(item) !== installationKey(normalized));
|
|
19
|
+
writeInstallations([...installations, normalized], filePath);
|
|
20
|
+
}
|
|
21
|
+
export function forgetInstallation(installation, filePath = installationsPath()) {
|
|
22
|
+
const key = installationKey(normalizeInstallation(installation));
|
|
23
|
+
writeInstallations(readInstallations(filePath).filter((item) => installationKey(item) !== key), filePath);
|
|
24
|
+
}
|
|
25
|
+
export function replaceInstallations(installations, filePath = installationsPath()) {
|
|
26
|
+
writeInstallations(installations.map(normalizeInstallation), filePath);
|
|
27
|
+
}
|
|
28
|
+
function normalizeInstallation(installation) {
|
|
29
|
+
return { ...installation, workspace: path.resolve(installation.workspace) };
|
|
30
|
+
}
|
|
31
|
+
function installationKey(installation) {
|
|
32
|
+
return `${installation.target}\0${installation.scope}\0${path.resolve(installation.workspace)}`;
|
|
33
|
+
}
|
|
34
|
+
function writeInstallations(installations, filePath) {
|
|
35
|
+
if (installations.length === 0) {
|
|
36
|
+
fs.rmSync(filePath, { force: true });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
ensureFileDir(filePath);
|
|
40
|
+
fs.writeFileSync(filePath, `${JSON.stringify({ installations }, null, 2)}\n`, { mode: 0o600 });
|
|
41
|
+
}
|
|
@@ -2,27 +2,15 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { readJsonFile, writeJsonFile } from './json.js';
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export function otMcpConfig() {
|
|
8
|
-
return { command: 'ot', args: ['mcp', 'serve'], enabled: true };
|
|
9
|
-
}
|
|
10
|
-
export function mergeJsonMcpServer(filePath, name = MCP_SERVER_NAME) {
|
|
11
|
-
const parsed = readJsonFile(filePath, {});
|
|
12
|
-
const servers = parsed.mcpServers && typeof parsed.mcpServers === 'object'
|
|
13
|
-
? parsed.mcpServers
|
|
14
|
-
: {};
|
|
15
|
-
servers[name] = otMcpConfig();
|
|
16
|
-
writeJsonFile(filePath, { ...parsed, mcpServers: servers });
|
|
17
|
-
}
|
|
18
|
-
export function removeJsonMcpServer(filePath, name = MCP_SERVER_NAME, pruneStopDir) {
|
|
5
|
+
const LEGACY_SERVER_NAME = 'ot-cli';
|
|
6
|
+
export function removeLegacyJsonServer(filePath, pruneStopDir) {
|
|
19
7
|
if (!fs.existsSync(filePath))
|
|
20
8
|
return;
|
|
21
9
|
const parsed = readJsonFile(filePath, {});
|
|
22
10
|
const servers = parsed.mcpServers && typeof parsed.mcpServers === 'object'
|
|
23
11
|
? { ...parsed.mcpServers }
|
|
24
12
|
: {};
|
|
25
|
-
delete servers[
|
|
13
|
+
delete servers[LEGACY_SERVER_NAME];
|
|
26
14
|
const next = { ...parsed, mcpServers: servers };
|
|
27
15
|
if (Object.keys(servers).length === 0)
|
|
28
16
|
delete next.mcpServers;
|
|
@@ -33,7 +21,7 @@ export function removeJsonMcpServer(filePath, name = MCP_SERVER_NAME, pruneStopD
|
|
|
33
21
|
}
|
|
34
22
|
writeJsonFile(filePath, next);
|
|
35
23
|
}
|
|
36
|
-
export function
|
|
24
|
+
export function claudeLegacyServerPath(scope, workspace, homeDir = os.homedir()) {
|
|
37
25
|
return scope === 'user'
|
|
38
26
|
? path.join(homeDir, '.claude', 'mcp_servers.json')
|
|
39
27
|
: path.join(workspace, '.claude', 'mcp_servers.json');
|
|
@@ -46,42 +34,23 @@ export function claudeSettingsPath(scope, workspace, homeDir = os.homedir()) {
|
|
|
46
34
|
export function codexConfigPath(scope, workspace, homeDir = os.homedir()) {
|
|
47
35
|
return scope === 'user' ? path.join(homeDir, '.codex', 'config.toml') : path.join(workspace, '.codex', 'config.toml');
|
|
48
36
|
}
|
|
49
|
-
export function
|
|
37
|
+
export function cursorLegacyServerPath(workspace) {
|
|
50
38
|
return path.join(workspace, '.mcp.json');
|
|
51
39
|
}
|
|
52
|
-
export function
|
|
40
|
+
export function qoderLegacyServerPath(scope, workspace, homeDir = os.homedir()) {
|
|
53
41
|
return scope === 'user' ? path.join(homeDir, '.qoder.json') : path.join(workspace, '.mcp.json');
|
|
54
42
|
}
|
|
55
|
-
export function
|
|
43
|
+
export function kiroLegacyServerPath(scope, workspace, homeDir = os.homedir()) {
|
|
56
44
|
return scope === 'user'
|
|
57
45
|
? path.join(homeDir, '.kiro', 'settings', 'mcp.json')
|
|
58
46
|
: path.join(workspace, '.kiro', 'settings', 'mcp.json');
|
|
59
47
|
}
|
|
60
|
-
export function
|
|
61
|
-
ensureFileDir(filePath);
|
|
62
|
-
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
63
|
-
const withoutServer = stripTomlTable(existing, `mcp_servers.${MCP_SERVER_NAME}`);
|
|
64
|
-
const block = [
|
|
65
|
-
`[mcp_servers.${MCP_SERVER_NAME}]`,
|
|
66
|
-
`command = "ot"`,
|
|
67
|
-
`args = ["mcp", "serve"]`,
|
|
68
|
-
`enabled = true`,
|
|
69
|
-
].join('\n');
|
|
70
|
-
fs.writeFileSync(filePath, `${[withoutServer.trimEnd(), block].filter(Boolean).join('\n\n')}\n`, 'utf-8');
|
|
71
|
-
}
|
|
72
|
-
export function appendCodexHookBlocks(filePath, hookBlocks) {
|
|
73
|
-
ensureFileDir(filePath);
|
|
74
|
-
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
75
|
-
const cleaned = removeManagedTomlBlock(existing, 'hooks');
|
|
76
|
-
const block = [`# >>> OPENTURTLE-CLI:hooks`, hookBlocks.trimEnd(), `# <<< OPENTURTLE-CLI:hooks`].join('\n');
|
|
77
|
-
fs.writeFileSync(filePath, `${[cleaned.trimEnd(), block].filter(Boolean).join('\n\n')}\n`, 'utf-8');
|
|
78
|
-
}
|
|
79
|
-
export function removeCodexManagedConfig(filePath, pruneStopDir) {
|
|
48
|
+
export function removeLegacyCodexConfig(filePath, pruneStopDir) {
|
|
80
49
|
if (!fs.existsSync(filePath))
|
|
81
50
|
return;
|
|
82
51
|
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
83
52
|
const withoutHooks = removeManagedTomlBlock(existing, 'hooks');
|
|
84
|
-
const withoutServer = stripTomlTable(withoutHooks, `mcp_servers.${
|
|
53
|
+
const withoutServer = stripTomlTable(withoutHooks, `mcp_servers.${LEGACY_SERVER_NAME}`);
|
|
85
54
|
const next = withoutServer.trim();
|
|
86
55
|
if (!next) {
|
|
87
56
|
fs.rmSync(filePath, { force: true });
|
|
@@ -99,9 +68,8 @@ function stripTomlTable(content, tableName) {
|
|
|
99
68
|
let skipping = false;
|
|
100
69
|
const tablePattern = new RegExp(`^\\s*\\[${escapeRegExp(tableName)}\\]\\s*$`);
|
|
101
70
|
for (const line of lines) {
|
|
102
|
-
if (/^\s*\[/.test(line))
|
|
71
|
+
if (/^\s*\[/.test(line))
|
|
103
72
|
skipping = tablePattern.test(line);
|
|
104
|
-
}
|
|
105
73
|
if (!skipping)
|
|
106
74
|
kept.push(line);
|
|
107
75
|
}
|
package/dist/core/store.js
CHANGED
|
@@ -5,14 +5,13 @@ import { projectWorklogDir } from './paths.js';
|
|
|
5
5
|
export function initProjectStore(workspacePath) {
|
|
6
6
|
const workspace = path.resolve(workspacePath);
|
|
7
7
|
const dir = projectWorklogDir(workspace);
|
|
8
|
-
fs.mkdirSync(path.join(dir, 'hooks'), { recursive: true });
|
|
9
8
|
fs.mkdirSync(path.join(dir, 'drafts'), { recursive: true });
|
|
10
9
|
fs.mkdirSync(path.join(dir, 'cache'), { recursive: true });
|
|
11
10
|
const configPath = path.join(dir, 'config.json');
|
|
12
11
|
if (!fs.existsSync(configPath)) {
|
|
13
12
|
fs.writeFileSync(configPath, `${JSON.stringify({
|
|
14
13
|
version: 1,
|
|
15
|
-
|
|
14
|
+
integration: 'cli-skill',
|
|
16
15
|
createdAt: new Date().toISOString(),
|
|
17
16
|
}, null, 2)}\n`, 'utf-8');
|
|
18
17
|
}
|
|
@@ -37,17 +36,6 @@ function migrate(db) {
|
|
|
37
36
|
dedupe_key TEXT NOT NULL UNIQUE
|
|
38
37
|
);
|
|
39
38
|
|
|
40
|
-
CREATE TABLE IF NOT EXISTS hook_runs (
|
|
41
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
42
|
-
target TEXT NOT NULL,
|
|
43
|
-
event_type TEXT NOT NULL,
|
|
44
|
-
exit_code INTEGER NOT NULL,
|
|
45
|
-
duration_ms INTEGER NOT NULL,
|
|
46
|
-
raw_input_json TEXT,
|
|
47
|
-
error TEXT,
|
|
48
|
-
occurred_at TEXT NOT NULL
|
|
49
|
-
);
|
|
50
|
-
|
|
51
39
|
CREATE TABLE IF NOT EXISTS commits (
|
|
52
40
|
hash TEXT PRIMARY KEY,
|
|
53
41
|
message TEXT,
|
|
@@ -81,13 +69,6 @@ export function upsertEvent(store, event) {
|
|
|
81
69
|
.run(event.type, event.source, event.sourceId || null, event.workspacePath || null, event.summary || null, event.rawJson == null ? null : JSON.stringify(event.rawJson), occurredAt, event.dedupeKey);
|
|
82
70
|
return { inserted: result.changes > 0 };
|
|
83
71
|
}
|
|
84
|
-
export function recordHookRun(store, run) {
|
|
85
|
-
store.db
|
|
86
|
-
.prepare(`INSERT INTO hook_runs
|
|
87
|
-
(target, event_type, exit_code, duration_ms, raw_input_json, error, occurred_at)
|
|
88
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
89
|
-
.run(run.target, run.eventType, run.exitCode, run.durationMs, run.rawInputJson || null, run.error || null, run.occurredAt || new Date().toISOString());
|
|
90
|
-
}
|
|
91
72
|
export function listEvents(store, limit = 100) {
|
|
92
73
|
return store.db
|
|
93
74
|
.prepare('SELECT * FROM events ORDER BY occurred_at ASC, id ASC LIMIT ?')
|
package/dist/index.js
CHANGED
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { registerCollaborationCommands } from './commands/collaboration.js';
|
|
5
|
-
import {
|
|
5
|
+
import { registerLocalKnowledgeCommands, searchKnowledgeEverywhere } from './commands/local-knowledge.js';
|
|
6
6
|
import { doctor, draftDailyReport, createResourceCommands } from './commands/resources.js';
|
|
7
7
|
import { ApiClient, ApiError } from './core/api-client.js';
|
|
8
8
|
import { scheduleAutomaticUpdate, updateCli } from './core/auto-update.js';
|
|
9
9
|
import { clearAuth, readAuth, redactToken, writeAuth } from './core/auth.js';
|
|
10
10
|
import { DEFAULT_SERVER_URL } from './core/defaults.js';
|
|
11
|
+
import { forgetInstallation, readInstallations, recordInstallation, replaceInstallations, } from './core/installations.js';
|
|
11
12
|
import { findWorkspaceRoot } from './core/paths.js';
|
|
12
13
|
import { initProjectStore } from './core/store.js';
|
|
13
14
|
import { cliVersion } from './core/version.js';
|
|
14
15
|
import { loginWithWeb } from './core/web-auth.js';
|
|
15
16
|
import { installClaude, installCodex, installCursor, installKiro, installQoder, uninstallClaude, uninstallCodex, uninstallCursor, uninstallKiro, uninstallQoder, } from './installers/agents.js';
|
|
16
|
-
import {
|
|
17
|
+
import { uninstallLegacyGitHook } from './installers/legacy-git-cleanup.js';
|
|
17
18
|
import { uninstallAllLocalState } from './installers/uninstall.js';
|
|
18
|
-
import { serveMcp } from './mcp/server.js';
|
|
19
|
-
import { candidateHistoryPaths, importHistory } from './watchers/importers.js';
|
|
20
19
|
const program = new Command();
|
|
21
20
|
program
|
|
22
21
|
.name('ot')
|
|
@@ -125,17 +124,17 @@ program
|
|
|
125
124
|
.command('install')
|
|
126
125
|
.option('--target <target>')
|
|
127
126
|
.option('--scope <scope>', 'project|user', 'project')
|
|
128
|
-
.option('--with-agent-hooks')
|
|
129
127
|
.action(action((options) => {
|
|
130
128
|
requireOption(options.target, '--target');
|
|
131
129
|
const workspace = findWorkspaceRoot();
|
|
132
130
|
for (const target of expandTargets(options.target)) {
|
|
133
|
-
installTarget(target, workspace, options.scope
|
|
131
|
+
installTarget(target, workspace, options.scope);
|
|
134
132
|
}
|
|
135
133
|
print({
|
|
136
134
|
installed: options.target,
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
scope: options.scope,
|
|
136
|
+
integration: 'cli-skill',
|
|
137
|
+
legacy_mcp_and_hooks_removed: true,
|
|
139
138
|
});
|
|
140
139
|
}));
|
|
141
140
|
program
|
|
@@ -151,52 +150,6 @@ program
|
|
|
151
150
|
const local_state = options.target === 'all' ? uninstallAllLocalState(workspace) : { removedProjectDir: false };
|
|
152
151
|
print({ uninstalled: options.target, scope: options.scope, local_state });
|
|
153
152
|
}));
|
|
154
|
-
const hook = program.command('hook');
|
|
155
|
-
hook
|
|
156
|
-
.command('ingest')
|
|
157
|
-
.option('--target <target>')
|
|
158
|
-
.option('--event <event>')
|
|
159
|
-
.option('--stdin-json')
|
|
160
|
-
.action(action(async (options) => {
|
|
161
|
-
requireOption(options.target, '--target');
|
|
162
|
-
requireOption(options.event, '--event');
|
|
163
|
-
const stdin = options.stdinJson ? await readStdin() : '{}';
|
|
164
|
-
const result = ingestRawHook({
|
|
165
|
-
target: options.target,
|
|
166
|
-
event: options.event,
|
|
167
|
-
stdin,
|
|
168
|
-
workspace: findWorkspaceRoot(),
|
|
169
|
-
});
|
|
170
|
-
if (!result.ok)
|
|
171
|
-
process.exitCode = 0;
|
|
172
|
-
}));
|
|
173
|
-
hook.command('git-post-commit').action(action(() => {
|
|
174
|
-
recordGitPostCommit(findWorkspaceRoot());
|
|
175
|
-
}));
|
|
176
|
-
hook.command('doctor').action(action(() => print(doctor(findWorkspaceRoot()))));
|
|
177
|
-
program
|
|
178
|
-
.command('import-history')
|
|
179
|
-
.option('--target <target>')
|
|
180
|
-
.action(action((options) => {
|
|
181
|
-
requireOption(options.target, '--target');
|
|
182
|
-
const workspace = findWorkspaceRoot();
|
|
183
|
-
if (options.target === 'all') {
|
|
184
|
-
print(['claude', 'codex', 'cursor', 'qoder', 'kiro'].map((target) => importHistory(target, workspace)));
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
print(importHistory(options.target, workspace));
|
|
188
|
-
}));
|
|
189
|
-
program
|
|
190
|
-
.command('watch')
|
|
191
|
-
.option('--target <target>')
|
|
192
|
-
.action(action((options) => {
|
|
193
|
-
requireOption(options.target, '--target');
|
|
194
|
-
print({
|
|
195
|
-
target: options.target,
|
|
196
|
-
watcher_paths: candidateHistoryPaths(options.target),
|
|
197
|
-
note: 'v1 watch uses import-history polling semantics; native hooks are preferred where supported.',
|
|
198
|
-
});
|
|
199
|
-
}));
|
|
200
153
|
const resources = createResourceCommands();
|
|
201
154
|
const goals = program.command('goals');
|
|
202
155
|
goals
|
|
@@ -281,8 +234,11 @@ knowledge
|
|
|
281
234
|
.option('--person <name-or-id>')
|
|
282
235
|
.option('--scope <scope>', 'team|managed|participating|assigned', 'team')
|
|
283
236
|
.option('--limit <number>', 'maximum results', Number)
|
|
237
|
+
.option('--match <mode>', 'local match mode: any|all|exact', 'any')
|
|
238
|
+
.option('--cloud-only')
|
|
239
|
+
.option('--local-only')
|
|
284
240
|
.option('--json')
|
|
285
|
-
.action(action(async (keyword, options) => print(await resources.searchKnowledge(keyword, options), options.json)));
|
|
241
|
+
.action(action(async (keyword, options) => print(await searchKnowledgeEverywhere(keyword, options, () => resources.searchKnowledge(keyword, options)), options.json)));
|
|
286
242
|
knowledge
|
|
287
243
|
.command('sources')
|
|
288
244
|
.option('--path <path>')
|
|
@@ -293,6 +249,7 @@ knowledge
|
|
|
293
249
|
.command('source <path>')
|
|
294
250
|
.option('--json')
|
|
295
251
|
.action(action(async (filePath, options) => print(await resources.readKnowledgeSource(filePath), options.json)));
|
|
252
|
+
registerLocalKnowledgeCommands(knowledge, { print, fatal });
|
|
296
253
|
const worklogs = program.command('worklogs');
|
|
297
254
|
worklogs
|
|
298
255
|
.command('list')
|
|
@@ -339,16 +296,8 @@ program
|
|
|
339
296
|
throw new Error('Usage: ot report draft [--today] [--output <path>]');
|
|
340
297
|
}));
|
|
341
298
|
program
|
|
342
|
-
.command('
|
|
343
|
-
.
|
|
344
|
-
.allowUnknownOption()
|
|
345
|
-
.action(action(async (subcommand) => {
|
|
346
|
-
if (subcommand === 'serve') {
|
|
347
|
-
await serveMcp();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
throw new Error('Usage: ot mcp serve');
|
|
351
|
-
}));
|
|
299
|
+
.command('_refresh-installs', { hidden: true })
|
|
300
|
+
.action(action(() => print({ refreshed: refreshRecordedInstallations() })));
|
|
352
301
|
scheduleAutomaticUpdate(cliVersion());
|
|
353
302
|
if (!runSpecialNestedCommand(process.argv)) {
|
|
354
303
|
try {
|
|
@@ -384,24 +333,26 @@ function fatal(err) {
|
|
|
384
333
|
}
|
|
385
334
|
process.exit(1);
|
|
386
335
|
}
|
|
387
|
-
function installTarget(target, workspace, scope
|
|
336
|
+
function installTarget(target, workspace, scope) {
|
|
388
337
|
if (target === 'git')
|
|
389
|
-
|
|
338
|
+
throw new Error('Git Hook installation has been removed; use uninstall --target git for cleanup');
|
|
339
|
+
const options = { workspace, scope };
|
|
390
340
|
if (target === 'claude')
|
|
391
|
-
installClaude(
|
|
341
|
+
installClaude(options);
|
|
392
342
|
if (target === 'codex')
|
|
393
|
-
installCodex(
|
|
343
|
+
installCodex(options);
|
|
394
344
|
if (target === 'cursor')
|
|
395
|
-
installCursor(
|
|
345
|
+
installCursor(options);
|
|
396
346
|
if (target === 'qoder')
|
|
397
|
-
installQoder(
|
|
347
|
+
installQoder(options);
|
|
398
348
|
if (target === 'kiro')
|
|
399
|
-
installKiro(
|
|
349
|
+
installKiro(options);
|
|
350
|
+
recordInstallation({ target, scope, workspace });
|
|
400
351
|
}
|
|
401
352
|
function uninstallTarget(target, workspace, scope) {
|
|
402
|
-
const options = { workspace, scope
|
|
353
|
+
const options = { workspace, scope };
|
|
403
354
|
if (target === 'git')
|
|
404
|
-
|
|
355
|
+
uninstallLegacyGitHook(workspace);
|
|
405
356
|
if (target === 'claude')
|
|
406
357
|
uninstallClaude(options);
|
|
407
358
|
if (target === 'codex')
|
|
@@ -412,6 +363,8 @@ function uninstallTarget(target, workspace, scope) {
|
|
|
412
363
|
uninstallQoder(options);
|
|
413
364
|
if (target === 'kiro')
|
|
414
365
|
uninstallKiro(options);
|
|
366
|
+
if (target !== 'git')
|
|
367
|
+
forgetInstallation({ target, scope, workspace });
|
|
415
368
|
}
|
|
416
369
|
function requireOption(value, name) {
|
|
417
370
|
if (value == null || value === '') {
|
|
@@ -420,7 +373,7 @@ function requireOption(value, name) {
|
|
|
420
373
|
}
|
|
421
374
|
function expandTargets(target) {
|
|
422
375
|
if (target === 'all')
|
|
423
|
-
return ['
|
|
376
|
+
return ['claude', 'codex', 'cursor', 'qoder', 'kiro'];
|
|
424
377
|
return [target];
|
|
425
378
|
}
|
|
426
379
|
function print(value, asJson = true) {
|
|
@@ -430,12 +383,6 @@ function print(value, asJson = true) {
|
|
|
430
383
|
}
|
|
431
384
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
432
385
|
}
|
|
433
|
-
async function readStdin() {
|
|
434
|
-
const chunks = [];
|
|
435
|
-
for await (const chunk of process.stdin)
|
|
436
|
-
chunks.push(Buffer.from(chunk));
|
|
437
|
-
return Buffer.concat(chunks).toString('utf-8');
|
|
438
|
-
}
|
|
439
386
|
function updateGitignore(workspace) {
|
|
440
387
|
const gitignorePath = `${workspace}/.gitignore`;
|
|
441
388
|
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
@@ -445,10 +392,6 @@ function updateGitignore(workspace) {
|
|
|
445
392
|
}
|
|
446
393
|
function runSpecialNestedCommand(argv) {
|
|
447
394
|
const [, , first, second, ...rest] = argv;
|
|
448
|
-
if (first === 'mcp' && second === 'serve') {
|
|
449
|
-
void serveMcp().catch(fatal);
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
395
|
if (first === 'report' && second === 'draft') {
|
|
453
396
|
const options = parseSimpleOptions(rest);
|
|
454
397
|
print({ path: draftDailyReport(findWorkspaceRoot(), options) });
|
|
@@ -457,6 +400,17 @@ function runSpecialNestedCommand(argv) {
|
|
|
457
400
|
}
|
|
458
401
|
return false;
|
|
459
402
|
}
|
|
403
|
+
function refreshRecordedInstallations() {
|
|
404
|
+
const kept = [];
|
|
405
|
+
for (const installation of readInstallations()) {
|
|
406
|
+
if (!fs.existsSync(installation.workspace))
|
|
407
|
+
continue;
|
|
408
|
+
installTarget(installation.target, installation.workspace, installation.scope);
|
|
409
|
+
kept.push(installation);
|
|
410
|
+
}
|
|
411
|
+
replaceInstallations(kept);
|
|
412
|
+
return kept.length;
|
|
413
|
+
}
|
|
460
414
|
function parseSimpleOptions(args) {
|
|
461
415
|
const options = {};
|
|
462
416
|
for (let index = 0; index < args.length; index += 1) {
|