@openturtle/cli 0.3.2 → 0.3.3
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 +27 -148
- package/dist/commands/resources.js +2 -5
- package/dist/core/auto-update.js +18 -1
- 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 +33 -84
- package/dist/installers/agents.js +84 -195
- package/dist/installers/legacy-git-cleanup.js +29 -0
- package/package.json +2 -2
- 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
package/dist/index.js
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { registerCollaborationCommands } from './commands/collaboration.js';
|
|
5
|
-
import { ingestRawHook, recordGitPostCommit } from './commands/hook.js';
|
|
6
5
|
import { doctor, draftDailyReport, createResourceCommands } from './commands/resources.js';
|
|
7
6
|
import { ApiClient, ApiError } from './core/api-client.js';
|
|
8
7
|
import { scheduleAutomaticUpdate, updateCli } from './core/auto-update.js';
|
|
9
8
|
import { clearAuth, readAuth, redactToken, writeAuth } from './core/auth.js';
|
|
10
9
|
import { DEFAULT_SERVER_URL } from './core/defaults.js';
|
|
10
|
+
import { forgetInstallation, readInstallations, recordInstallation, replaceInstallations, } from './core/installations.js';
|
|
11
11
|
import { findWorkspaceRoot } from './core/paths.js';
|
|
12
12
|
import { initProjectStore } from './core/store.js';
|
|
13
13
|
import { cliVersion } from './core/version.js';
|
|
14
14
|
import { loginWithWeb } from './core/web-auth.js';
|
|
15
15
|
import { installClaude, installCodex, installCursor, installKiro, installQoder, uninstallClaude, uninstallCodex, uninstallCursor, uninstallKiro, uninstallQoder, } from './installers/agents.js';
|
|
16
|
-
import {
|
|
16
|
+
import { uninstallLegacyGitHook } from './installers/legacy-git-cleanup.js';
|
|
17
17
|
import { uninstallAllLocalState } from './installers/uninstall.js';
|
|
18
|
-
import { serveMcp } from './mcp/server.js';
|
|
19
|
-
import { candidateHistoryPaths, importHistory } from './watchers/importers.js';
|
|
20
18
|
const program = new Command();
|
|
21
19
|
program
|
|
22
20
|
.name('ot')
|
|
@@ -125,17 +123,17 @@ program
|
|
|
125
123
|
.command('install')
|
|
126
124
|
.option('--target <target>')
|
|
127
125
|
.option('--scope <scope>', 'project|user', 'project')
|
|
128
|
-
.option('--with-agent-hooks')
|
|
129
126
|
.action(action((options) => {
|
|
130
127
|
requireOption(options.target, '--target');
|
|
131
128
|
const workspace = findWorkspaceRoot();
|
|
132
129
|
for (const target of expandTargets(options.target)) {
|
|
133
|
-
installTarget(target, workspace, options.scope
|
|
130
|
+
installTarget(target, workspace, options.scope);
|
|
134
131
|
}
|
|
135
132
|
print({
|
|
136
133
|
installed: options.target,
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
scope: options.scope,
|
|
135
|
+
integration: 'cli-skill',
|
|
136
|
+
legacy_mcp_and_hooks_removed: true,
|
|
139
137
|
});
|
|
140
138
|
}));
|
|
141
139
|
program
|
|
@@ -151,52 +149,6 @@ program
|
|
|
151
149
|
const local_state = options.target === 'all' ? uninstallAllLocalState(workspace) : { removedProjectDir: false };
|
|
152
150
|
print({ uninstalled: options.target, scope: options.scope, local_state });
|
|
153
151
|
}));
|
|
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
152
|
const resources = createResourceCommands();
|
|
201
153
|
const goals = program.command('goals');
|
|
202
154
|
goals
|
|
@@ -339,16 +291,8 @@ program
|
|
|
339
291
|
throw new Error('Usage: ot report draft [--today] [--output <path>]');
|
|
340
292
|
}));
|
|
341
293
|
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
|
-
}));
|
|
294
|
+
.command('_refresh-installs', { hidden: true })
|
|
295
|
+
.action(action(() => print({ refreshed: refreshRecordedInstallations() })));
|
|
352
296
|
scheduleAutomaticUpdate(cliVersion());
|
|
353
297
|
if (!runSpecialNestedCommand(process.argv)) {
|
|
354
298
|
try {
|
|
@@ -384,24 +328,26 @@ function fatal(err) {
|
|
|
384
328
|
}
|
|
385
329
|
process.exit(1);
|
|
386
330
|
}
|
|
387
|
-
function installTarget(target, workspace, scope
|
|
331
|
+
function installTarget(target, workspace, scope) {
|
|
388
332
|
if (target === 'git')
|
|
389
|
-
|
|
333
|
+
throw new Error('Git Hook installation has been removed; use uninstall --target git for cleanup');
|
|
334
|
+
const options = { workspace, scope };
|
|
390
335
|
if (target === 'claude')
|
|
391
|
-
installClaude(
|
|
336
|
+
installClaude(options);
|
|
392
337
|
if (target === 'codex')
|
|
393
|
-
installCodex(
|
|
338
|
+
installCodex(options);
|
|
394
339
|
if (target === 'cursor')
|
|
395
|
-
installCursor(
|
|
340
|
+
installCursor(options);
|
|
396
341
|
if (target === 'qoder')
|
|
397
|
-
installQoder(
|
|
342
|
+
installQoder(options);
|
|
398
343
|
if (target === 'kiro')
|
|
399
|
-
installKiro(
|
|
344
|
+
installKiro(options);
|
|
345
|
+
recordInstallation({ target, scope, workspace });
|
|
400
346
|
}
|
|
401
347
|
function uninstallTarget(target, workspace, scope) {
|
|
402
|
-
const options = { workspace, scope
|
|
348
|
+
const options = { workspace, scope };
|
|
403
349
|
if (target === 'git')
|
|
404
|
-
|
|
350
|
+
uninstallLegacyGitHook(workspace);
|
|
405
351
|
if (target === 'claude')
|
|
406
352
|
uninstallClaude(options);
|
|
407
353
|
if (target === 'codex')
|
|
@@ -412,6 +358,8 @@ function uninstallTarget(target, workspace, scope) {
|
|
|
412
358
|
uninstallQoder(options);
|
|
413
359
|
if (target === 'kiro')
|
|
414
360
|
uninstallKiro(options);
|
|
361
|
+
if (target !== 'git')
|
|
362
|
+
forgetInstallation({ target, scope, workspace });
|
|
415
363
|
}
|
|
416
364
|
function requireOption(value, name) {
|
|
417
365
|
if (value == null || value === '') {
|
|
@@ -420,7 +368,7 @@ function requireOption(value, name) {
|
|
|
420
368
|
}
|
|
421
369
|
function expandTargets(target) {
|
|
422
370
|
if (target === 'all')
|
|
423
|
-
return ['
|
|
371
|
+
return ['claude', 'codex', 'cursor', 'qoder', 'kiro'];
|
|
424
372
|
return [target];
|
|
425
373
|
}
|
|
426
374
|
function print(value, asJson = true) {
|
|
@@ -430,12 +378,6 @@ function print(value, asJson = true) {
|
|
|
430
378
|
}
|
|
431
379
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
432
380
|
}
|
|
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
381
|
function updateGitignore(workspace) {
|
|
440
382
|
const gitignorePath = `${workspace}/.gitignore`;
|
|
441
383
|
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
@@ -445,10 +387,6 @@ function updateGitignore(workspace) {
|
|
|
445
387
|
}
|
|
446
388
|
function runSpecialNestedCommand(argv) {
|
|
447
389
|
const [, , first, second, ...rest] = argv;
|
|
448
|
-
if (first === 'mcp' && second === 'serve') {
|
|
449
|
-
void serveMcp().catch(fatal);
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
390
|
if (first === 'report' && second === 'draft') {
|
|
453
391
|
const options = parseSimpleOptions(rest);
|
|
454
392
|
print({ path: draftDailyReport(findWorkspaceRoot(), options) });
|
|
@@ -457,6 +395,17 @@ function runSpecialNestedCommand(argv) {
|
|
|
457
395
|
}
|
|
458
396
|
return false;
|
|
459
397
|
}
|
|
398
|
+
function refreshRecordedInstallations() {
|
|
399
|
+
const kept = [];
|
|
400
|
+
for (const installation of readInstallations()) {
|
|
401
|
+
if (!fs.existsSync(installation.workspace))
|
|
402
|
+
continue;
|
|
403
|
+
installTarget(installation.target, installation.workspace, installation.scope);
|
|
404
|
+
kept.push(installation);
|
|
405
|
+
}
|
|
406
|
+
replaceInstallations(kept);
|
|
407
|
+
return kept.length;
|
|
408
|
+
}
|
|
460
409
|
function parseSimpleOptions(args) {
|
|
461
410
|
const options = {};
|
|
462
411
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -1,158 +1,116 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { appendCodexHookBlocks, claudeMcpPath, claudeSettingsPath, codexConfigPath, cursorMcpPath, kiroMcpPath, mergeCodexMcpServer, mergeJsonMcpServer, qoderMcpPath, removeCodexManagedConfig, removeJsonMcpServer, } from '../core/mcp-config.js';
|
|
4
|
+
import { readJsonFile } from '../core/json.js';
|
|
5
|
+
import { claudeLegacyServerPath, claudeSettingsPath, codexConfigPath, cursorLegacyServerPath, kiroLegacyServerPath, qoderLegacyServerPath, removeLegacyCodexConfig, removeLegacyJsonServer, } from '../core/legacy-agent-cleanup.js';
|
|
7
6
|
import { ensureFileDir } from '../core/paths.js';
|
|
8
7
|
import { removeCursorHookEntries, removeDirIfExists, removeFileIfExists, removeManagedHookEntries, writeJsonOrRemoveEmpty, } from './uninstall.js';
|
|
9
8
|
export function installClaude(options) {
|
|
10
|
-
|
|
11
|
-
writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md')
|
|
12
|
-
if (options.withAgentHooks) {
|
|
13
|
-
mergeJsonMcpServerInSettings(claudeSettingsPath(options.scope, options.workspace, options.homeDir));
|
|
14
|
-
const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
|
|
15
|
-
const settings = readJsonFile(settingsPath, {});
|
|
16
|
-
const hooks = settings.hooks || {};
|
|
17
|
-
settings.hooks = {
|
|
18
|
-
...hooks,
|
|
19
|
-
UserPromptSubmit: mergeManagedHookArray(hooks.UserPromptSubmit, managedHook('claude', 'UserPromptSubmit')),
|
|
20
|
-
PreToolUse: mergeManagedHookArray(hooks.PreToolUse, managedHook('claude', 'PreToolUse', '*')),
|
|
21
|
-
PostToolUse: mergeManagedHookArray(hooks.PostToolUse, managedHook('claude', 'PostToolUse', '*')),
|
|
22
|
-
Stop: mergeManagedHookArray(hooks.Stop, managedHook('claude', 'Stop')),
|
|
23
|
-
};
|
|
24
|
-
writeJsonFile(settingsPath, settings);
|
|
25
|
-
}
|
|
9
|
+
cleanupClaudeLegacy(options);
|
|
10
|
+
writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
26
11
|
}
|
|
27
12
|
export function installCodex(options) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (options.withAgentHooks) {
|
|
33
|
-
appendCodexHookBlocks(configPath, [
|
|
34
|
-
codexHookBlock('PreToolUse', 'codex', 'PreToolUse'),
|
|
35
|
-
codexHookBlock('PostToolUse', 'codex', 'PostToolUse'),
|
|
36
|
-
codexHookBlock('Stop', 'codex', 'Stop'),
|
|
37
|
-
].join('\n\n'));
|
|
38
|
-
}
|
|
13
|
+
cleanupCodexLegacy(options);
|
|
14
|
+
writeSkill(path.join(rootForScope(options, '.codex'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
15
|
+
if (options.scope === 'project')
|
|
16
|
+
appendAgentsSnippet(options.workspace);
|
|
39
17
|
}
|
|
40
18
|
export function installCursor(options) {
|
|
41
|
-
|
|
19
|
+
cleanupCursorLegacy(options);
|
|
42
20
|
writeTextFile(path.join(options.workspace, '.cursor', 'rules', 'openturtle-cli.mdc'), [
|
|
43
21
|
'---',
|
|
44
|
-
'description: Use OpenTurtle CLI
|
|
22
|
+
'description: Use the OpenTurtle CLI directly for projects, goals, todos, knowledge, meetings, and worklogs.',
|
|
45
23
|
'alwaysApply: true',
|
|
46
24
|
'---',
|
|
47
25
|
'',
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
26
|
+
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
27
|
+
'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
|
|
28
|
+
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
29
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
51
30
|
'',
|
|
52
31
|
].join('\n'));
|
|
53
|
-
if (options.withAgentHooks) {
|
|
54
|
-
const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
|
|
55
|
-
const hooks = readJsonFile(hooksPath, {});
|
|
56
|
-
hooks.preToolUse = mergeManagedHookArray(hooks.preToolUse, cursorHook('cursor', 'pre_tool_use'));
|
|
57
|
-
hooks.postToolUse = mergeManagedHookArray(hooks.postToolUse, cursorHook('cursor', 'post_tool_use'));
|
|
58
|
-
writeJsonFile(hooksPath, hooks);
|
|
59
|
-
}
|
|
60
32
|
}
|
|
61
33
|
export function installQoder(options) {
|
|
62
|
-
|
|
63
|
-
writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md')
|
|
64
|
-
if (options.withAgentHooks) {
|
|
65
|
-
const settingsPath = options.scope === 'user'
|
|
66
|
-
? path.join(options.homeDir || os.homedir(), '.qoder', 'settings.json')
|
|
67
|
-
: path.join(options.workspace, '.qoder', 'settings.local.json');
|
|
68
|
-
const settings = readJsonFile(settingsPath, {});
|
|
69
|
-
const hooks = settings.hooks || {};
|
|
70
|
-
settings.hooks = {
|
|
71
|
-
...hooks,
|
|
72
|
-
UserPromptSubmit: mergeManagedHookArray(hooks.UserPromptSubmit, managedHook('qoder', 'UserPromptSubmit')),
|
|
73
|
-
PreToolUse: mergeManagedHookArray(hooks.PreToolUse, managedHook('qoder', 'PreToolUse', '*')),
|
|
74
|
-
PostToolUse: mergeManagedHookArray(hooks.PostToolUse, managedHook('qoder', 'PostToolUse', '*')),
|
|
75
|
-
PostToolUseFailure: mergeManagedHookArray(hooks.PostToolUseFailure, managedHook('qoder', 'PostToolUseFailure', '*')),
|
|
76
|
-
Stop: mergeManagedHookArray(hooks.Stop, managedHook('qoder', 'Stop')),
|
|
77
|
-
};
|
|
78
|
-
writeJsonFile(settingsPath, settings);
|
|
79
|
-
}
|
|
34
|
+
cleanupQoderLegacy(options);
|
|
35
|
+
writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
80
36
|
}
|
|
81
37
|
export function installKiro(options) {
|
|
82
|
-
|
|
38
|
+
cleanupKiroLegacy(options);
|
|
83
39
|
writeTextFile(path.join(rootForScope(options, '.kiro'), 'steering', 'openturtle-cli.md'), [
|
|
84
40
|
'# OpenTurtle CLI',
|
|
85
41
|
'',
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
42
|
+
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
43
|
+
'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
|
|
44
|
+
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
45
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
89
46
|
'',
|
|
90
47
|
].join('\n'));
|
|
91
|
-
if (options.withAgentHooks) {
|
|
92
|
-
const agentPath = path.join(rootForScope(options, '.kiro'), 'agents', 'openturtle-cli.json');
|
|
93
|
-
const agent = readJsonFile(agentPath, {});
|
|
94
|
-
const hooks = agent.hooks || {};
|
|
95
|
-
agent.name = 'openturtle-cli';
|
|
96
|
-
agent.description = 'OpenTurtle local audit and MCP integration';
|
|
97
|
-
agent.hooks = {
|
|
98
|
-
...hooks,
|
|
99
|
-
agentSpawn: managedKiroHookList('agentSpawn'),
|
|
100
|
-
userPromptSubmit: managedKiroHookList('userPromptSubmit'),
|
|
101
|
-
preToolUse: managedKiroHookList('preToolUse', '*'),
|
|
102
|
-
postToolUse: managedKiroHookList('postToolUse', '*'),
|
|
103
|
-
stop: managedKiroHookList('stop'),
|
|
104
|
-
};
|
|
105
|
-
writeJsonFile(agentPath, agent);
|
|
106
|
-
writeKiroIdeHook(path.join(rootForScope(options, '.kiro'), 'hooks', 'openturtle-cli-audit.kiro.hook'));
|
|
107
|
-
}
|
|
108
48
|
}
|
|
109
49
|
export function uninstallClaude(options) {
|
|
50
|
+
cleanupClaudeLegacy(options);
|
|
110
51
|
const root = rootForScope(options, '.claude');
|
|
111
|
-
removeJsonMcpServer(claudeMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', root);
|
|
112
|
-
const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
|
|
113
|
-
if (fs.existsSync(settingsPath)) {
|
|
114
|
-
const settings = readJsonFile(settingsPath, {});
|
|
115
|
-
if (settings.mcpServers && typeof settings.mcpServers === 'object') {
|
|
116
|
-
delete settings.mcpServers['ot-cli'];
|
|
117
|
-
}
|
|
118
|
-
writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
|
|
119
|
-
}
|
|
120
52
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
121
53
|
}
|
|
122
54
|
export function uninstallCodex(options) {
|
|
55
|
+
cleanupCodexLegacy(options);
|
|
123
56
|
const root = rootForScope(options, '.codex');
|
|
124
|
-
removeCodexManagedConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
|
|
125
57
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
126
|
-
|
|
58
|
+
if (options.scope === 'project')
|
|
59
|
+
removeAgentsSnippet(options.workspace);
|
|
127
60
|
}
|
|
128
61
|
export function uninstallCursor(options) {
|
|
129
|
-
|
|
62
|
+
cleanupCursorLegacy(options);
|
|
130
63
|
const root = path.join(options.workspace, '.cursor');
|
|
131
64
|
removeFileIfExists(path.join(root, 'rules', 'openturtle-cli.mdc'), root);
|
|
132
|
-
const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
|
|
133
|
-
if (fs.existsSync(hooksPath)) {
|
|
134
|
-
const hooks = readJsonFile(hooksPath, {});
|
|
135
|
-
writeJsonOrRemoveEmpty(hooksPath, removeCursorHookEntries(hooks), path.dirname(hooksPath));
|
|
136
|
-
}
|
|
137
65
|
}
|
|
138
66
|
export function uninstallQoder(options) {
|
|
67
|
+
cleanupQoderLegacy(options);
|
|
139
68
|
const root = rootForScope(options, '.qoder');
|
|
140
|
-
removeJsonMcpServer(qoderMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', options.scope === 'user'
|
|
141
|
-
? path.dirname(qoderMcpPath(options.scope, options.workspace, options.homeDir))
|
|
142
|
-
: options.workspace);
|
|
143
69
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
70
|
+
}
|
|
71
|
+
export function uninstallKiro(options) {
|
|
72
|
+
cleanupKiroLegacy(options);
|
|
73
|
+
const root = rootForScope(options, '.kiro');
|
|
74
|
+
removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
|
|
75
|
+
}
|
|
76
|
+
function cleanupClaudeLegacy(options) {
|
|
77
|
+
const root = rootForScope(options, '.claude');
|
|
78
|
+
removeLegacyJsonServer(claudeLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
|
|
79
|
+
const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
|
|
80
|
+
if (!fs.existsSync(settingsPath))
|
|
81
|
+
return;
|
|
82
|
+
const settings = readJsonFile(settingsPath, {});
|
|
83
|
+
if (settings.mcpServers && typeof settings.mcpServers === 'object')
|
|
84
|
+
delete settings.mcpServers['ot-cli'];
|
|
85
|
+
writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
|
|
86
|
+
}
|
|
87
|
+
function cleanupCodexLegacy(options) {
|
|
88
|
+
const root = rootForScope(options, '.codex');
|
|
89
|
+
removeLegacyCodexConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
|
|
90
|
+
}
|
|
91
|
+
function cleanupCursorLegacy(options) {
|
|
92
|
+
removeLegacyJsonServer(cursorLegacyServerPath(options.workspace), options.workspace);
|
|
93
|
+
const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
|
|
94
|
+
if (!fs.existsSync(hooksPath))
|
|
95
|
+
return;
|
|
96
|
+
const hooks = readJsonFile(hooksPath, {});
|
|
97
|
+
writeJsonOrRemoveEmpty(hooksPath, removeCursorHookEntries(hooks), path.dirname(hooksPath));
|
|
98
|
+
}
|
|
99
|
+
function cleanupQoderLegacy(options) {
|
|
100
|
+
const root = rootForScope(options, '.qoder');
|
|
101
|
+
const mcpPath = qoderLegacyServerPath(options.scope, options.workspace, options.homeDir);
|
|
102
|
+
removeLegacyJsonServer(mcpPath, options.scope === 'user' ? path.dirname(mcpPath) : options.workspace);
|
|
144
103
|
const settingsPath = options.scope === 'user'
|
|
145
104
|
? path.join(options.homeDir || os.homedir(), '.qoder', 'settings.json')
|
|
146
105
|
: path.join(options.workspace, '.qoder', 'settings.local.json');
|
|
147
|
-
if (fs.existsSync(settingsPath))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
106
|
+
if (!fs.existsSync(settingsPath))
|
|
107
|
+
return;
|
|
108
|
+
const settings = readJsonFile(settingsPath, {});
|
|
109
|
+
writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
|
|
151
110
|
}
|
|
152
|
-
|
|
111
|
+
function cleanupKiroLegacy(options) {
|
|
153
112
|
const root = rootForScope(options, '.kiro');
|
|
154
|
-
|
|
155
|
-
removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
|
|
113
|
+
removeLegacyJsonServer(kiroLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
|
|
156
114
|
removeFileIfExists(path.join(root, 'agents', 'openturtle-cli.json'), root);
|
|
157
115
|
removeFileIfExists(path.join(root, 'hooks', 'openturtle-cli-audit.kiro.hook'), root);
|
|
158
116
|
}
|
|
@@ -161,59 +119,11 @@ function rootForScope(options, dirName) {
|
|
|
161
119
|
? path.join(options.homeDir || os.homedir(), dirName)
|
|
162
120
|
: path.join(options.workspace, dirName);
|
|
163
121
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
const servers = settings.mcpServers && typeof settings.mcpServers === 'object'
|
|
167
|
-
? settings.mcpServers
|
|
168
|
-
: {};
|
|
169
|
-
servers['ot-cli'] = { command: 'ot', args: ['mcp', 'serve'], enabled: true };
|
|
170
|
-
writeJsonFile(settingsPath, { ...settings, mcpServers: servers });
|
|
171
|
-
}
|
|
172
|
-
function managedHook(target, event, matcher) {
|
|
173
|
-
return withoutUndefined({
|
|
174
|
-
id: 'OPENTURTLE-CLI',
|
|
175
|
-
name: 'OPENTURTLE-CLI audit',
|
|
176
|
-
matcher,
|
|
177
|
-
command: hookCommand(target, event),
|
|
178
|
-
timeout_ms: 2000,
|
|
179
|
-
continue_on_error: true,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
function managedKiroHookList(event, matcher) {
|
|
183
|
-
return [
|
|
184
|
-
withoutUndefined({
|
|
185
|
-
id: 'OPENTURTLE-CLI',
|
|
186
|
-
name: 'OPENTURTLE-CLI audit',
|
|
187
|
-
matcher,
|
|
188
|
-
command: hookCommand('kiro', event),
|
|
189
|
-
timeout_ms: 2000,
|
|
190
|
-
cache_ttl_seconds: 0,
|
|
191
|
-
continue_on_error: true,
|
|
192
|
-
}),
|
|
193
|
-
];
|
|
194
|
-
}
|
|
195
|
-
function cursorHook(target, event) {
|
|
196
|
-
return {
|
|
197
|
-
id: 'OPENTURTLE-CLI',
|
|
198
|
-
command: hookCommand(target, event),
|
|
199
|
-
timeout_ms: 2000,
|
|
200
|
-
continue_on_error: true,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
function mergeManagedHookArray(existing, entry) {
|
|
204
|
-
const values = Array.isArray(existing) ? existing.filter((item) => item?.id !== 'OPENTURTLE-CLI') : [];
|
|
205
|
-
return [...values, entry];
|
|
122
|
+
function writeSkill(filePath) {
|
|
123
|
+
writeTextFile(filePath, skillContent());
|
|
206
124
|
}
|
|
207
|
-
function
|
|
125
|
+
export function skillContent() {
|
|
208
126
|
return [
|
|
209
|
-
`[[hooks.${hook}]]`,
|
|
210
|
-
`command = "ot"`,
|
|
211
|
-
`args = ["hook", "ingest", "--target", "${target}", "--event", "${event}", "--stdin-json"]`,
|
|
212
|
-
`timeout_ms = 2000`,
|
|
213
|
-
].join('\n');
|
|
214
|
-
}
|
|
215
|
-
function writeSkill(filePath, agentName) {
|
|
216
|
-
writeTextFile(filePath, [
|
|
217
127
|
'---',
|
|
218
128
|
'name: openturtle-cli',
|
|
219
129
|
'description: Use OpenTurtle projects, team roster, project context, knowledge, meetings, goals, todos, and worklogs from an agent or terminal without requiring the Desktop client.',
|
|
@@ -221,10 +131,11 @@ function writeSkill(filePath, agentName) {
|
|
|
221
131
|
'',
|
|
222
132
|
'# OpenTurtle CLI',
|
|
223
133
|
'',
|
|
224
|
-
|
|
134
|
+
'Use the direct `ot` command for every OpenTurtle operation. Do not use an OpenTurtle MCP server and do not install OpenTurtle hooks.',
|
|
135
|
+
'Start with `command -v ot`. Run `ot --json doctor` before a remote workflow when authentication or server reachability is uncertain.',
|
|
225
136
|
'If authentication is missing, ask the user to run `ot auth login --web`; never request or expose their browser token.',
|
|
226
|
-
'Run `ot --json
|
|
227
|
-
'
|
|
137
|
+
'Run commands as `ot ... --json` so results are stable and machine-readable.',
|
|
138
|
+
'Before a write, run the same command with `--dry-run` and inspect `request.path` and `request.body`.',
|
|
228
139
|
'Treat knowledge as reusable memory objects. Treat source files as original materials referenced by knowledge, not as a second knowledge system.',
|
|
229
140
|
'',
|
|
230
141
|
'## Meeting workflow',
|
|
@@ -238,31 +149,29 @@ function writeSkill(filePath, agentName) {
|
|
|
238
149
|
'',
|
|
239
150
|
'## Todo quality',
|
|
240
151
|
'',
|
|
241
|
-
'When creating or proposing OpenTurtle todos
|
|
152
|
+
'When creating or proposing OpenTurtle todos, include source context, scope/environment, concrete action steps, measurable acceptance criteria, and required evidence.',
|
|
242
153
|
'Source context means the goal, meeting, plan artifact, commit, PR, file, worklog, or user request that caused the todo.',
|
|
243
|
-
'If the source context, environment, owner, due date, measurable acceptance criteria, or evidence is unclear, ask
|
|
154
|
+
'If the source context, environment, owner, due date, measurable acceptance criteria, or evidence is unclear, ask one concise question before creating the todo.',
|
|
244
155
|
'Avoid vague acceptance such as "no obvious delay", "works normally", or "verify it". Prefer concrete thresholds, environments, and evidence.',
|
|
245
156
|
'Use `--body-file <json>` or `--body-file -` for structured writes so shell quoting does not corrupt payloads.',
|
|
246
157
|
'Do not write to `.openturtle/`; CLI state lives in `.openturtle-cli/` and `~/.openturtle/cli/`.',
|
|
247
158
|
'',
|
|
248
|
-
].join('\n')
|
|
159
|
+
].join('\n');
|
|
249
160
|
}
|
|
250
161
|
function appendAgentsSnippet(workspace) {
|
|
251
162
|
const filePath = path.join(workspace, 'AGENTS.md');
|
|
252
163
|
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
253
|
-
const
|
|
254
|
-
if (existing.includes(marker))
|
|
255
|
-
return;
|
|
164
|
+
const withoutOld = existing.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
|
|
256
165
|
const snippet = [
|
|
257
|
-
|
|
166
|
+
'<!-- OPENTURTLE-CLI -->',
|
|
258
167
|
'',
|
|
259
|
-
'
|
|
260
|
-
'
|
|
261
|
-
'Todo quality:
|
|
168
|
+
'Use the direct `ot ... --json` CLI for OpenTurtle projects, goals, todos, knowledge, meetings, and worklogs. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
169
|
+
'Run `ot --json doctor` when authentication or server reachability is uncertain, and use `--dry-run` before writes.',
|
|
170
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
262
171
|
'',
|
|
263
172
|
'<!-- /OPENTURTLE-CLI -->',
|
|
264
173
|
].join('\n');
|
|
265
|
-
writeTextFile(filePath, `${
|
|
174
|
+
writeTextFile(filePath, `${withoutOld}${withoutOld ? '\n\n' : ''}${snippet}\n`);
|
|
266
175
|
}
|
|
267
176
|
function removeAgentsSnippet(workspace) {
|
|
268
177
|
const filePath = path.join(workspace, 'AGENTS.md');
|
|
@@ -270,32 +179,12 @@ function removeAgentsSnippet(workspace) {
|
|
|
270
179
|
return;
|
|
271
180
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
272
181
|
const cleaned = content.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
|
|
273
|
-
if (cleaned)
|
|
182
|
+
if (cleaned)
|
|
274
183
|
fs.writeFileSync(filePath, `${cleaned}\n`, 'utf-8');
|
|
275
|
-
|
|
276
|
-
else {
|
|
184
|
+
else
|
|
277
185
|
fs.rmSync(filePath, { force: true });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function writeKiroIdeHook(filePath) {
|
|
281
|
-
writeTextFile(filePath, [
|
|
282
|
-
'{',
|
|
283
|
-
' "name": "OpenTurtle CLI audit",',
|
|
284
|
-
' "description": "Record Kiro hook events into .openturtle-cli/state.db",',
|
|
285
|
-
' "event": "postToolUse",',
|
|
286
|
-
' "action": {',
|
|
287
|
-
' "type": "command",',
|
|
288
|
-
` "command": "${hookCommand('kiro', 'postToolUse').replaceAll('"', '\\"')}",`,
|
|
289
|
-
' "timeout_ms": 2000',
|
|
290
|
-
' }',
|
|
291
|
-
'}',
|
|
292
|
-
'',
|
|
293
|
-
].join('\n'));
|
|
294
186
|
}
|
|
295
187
|
function writeTextFile(filePath, content) {
|
|
296
188
|
ensureFileDir(filePath);
|
|
297
189
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
298
190
|
}
|
|
299
|
-
function withoutUndefined(value) {
|
|
300
|
-
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
301
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { projectWorklogDir } from '../core/paths.js';
|
|
4
|
+
const MARKER = 'OPENTURTLE-CLI';
|
|
5
|
+
export function uninstallLegacyGitHook(workspace) {
|
|
6
|
+
const hookPath = path.join(workspace, '.git', 'hooks', 'post-commit');
|
|
7
|
+
const originalPath = path.join(projectWorklogDir(workspace), 'hooks', 'git-post-commit.original');
|
|
8
|
+
if (!fs.existsSync(hookPath))
|
|
9
|
+
return;
|
|
10
|
+
const current = fs.readFileSync(hookPath, 'utf-8');
|
|
11
|
+
if (!current.includes(MARKER))
|
|
12
|
+
return;
|
|
13
|
+
if (fs.existsSync(originalPath)) {
|
|
14
|
+
fs.copyFileSync(originalPath, hookPath);
|
|
15
|
+
fs.chmodSync(hookPath, 0o755);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
fs.unlinkSync(hookPath);
|
|
19
|
+
}
|
|
20
|
+
fs.rmSync(originalPath, { force: true });
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(path.dirname(originalPath)) && fs.readdirSync(path.dirname(originalPath)).length === 0) {
|
|
23
|
+
fs.rmdirSync(path.dirname(originalPath));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Legacy cleanup should not fail because an auxiliary directory is not empty.
|
|
28
|
+
}
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openturtle/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "OpenTurtle collaboration, knowledge, meeting, and agent CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openturtle",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"registry": "https://registry.npmjs.org/"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"build": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
36
36
|
"prepublishOnly": "pnpm run test && pnpm run build",
|
|
37
37
|
"test": "tsx --test test/*.test.ts",
|
|
38
38
|
"dev": "tsx src/index.ts"
|