@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
|
@@ -1,158 +1,117 @@
|
|
|
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 { readJsonFile
|
|
6
|
-
import {
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { readJsonFile } from '../core/json.js';
|
|
6
|
+
import { claudeLegacyServerPath, claudeSettingsPath, codexConfigPath, cursorLegacyServerPath, kiroLegacyServerPath, qoderLegacyServerPath, removeLegacyCodexConfig, removeLegacyJsonServer, } from '../core/legacy-agent-cleanup.js';
|
|
7
7
|
import { ensureFileDir } from '../core/paths.js';
|
|
8
8
|
import { removeCursorHookEntries, removeDirIfExists, removeFileIfExists, removeManagedHookEntries, writeJsonOrRemoveEmpty, } from './uninstall.js';
|
|
9
9
|
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
|
-
}
|
|
10
|
+
cleanupClaudeLegacy(options);
|
|
11
|
+
writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
26
12
|
}
|
|
27
13
|
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
|
-
}
|
|
14
|
+
cleanupCodexLegacy(options);
|
|
15
|
+
writeSkill(path.join(rootForScope(options, '.codex'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
16
|
+
if (options.scope === 'project')
|
|
17
|
+
appendAgentsSnippet(options.workspace);
|
|
39
18
|
}
|
|
40
19
|
export function installCursor(options) {
|
|
41
|
-
|
|
20
|
+
cleanupCursorLegacy(options);
|
|
42
21
|
writeTextFile(path.join(options.workspace, '.cursor', 'rules', 'openturtle-cli.mdc'), [
|
|
43
22
|
'---',
|
|
44
|
-
'description: Use OpenTurtle CLI
|
|
23
|
+
'description: Use the OpenTurtle CLI directly for projects, goals, todos, knowledge, meetings, and worklogs.',
|
|
45
24
|
'alwaysApply: true',
|
|
46
25
|
'---',
|
|
47
26
|
'',
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
27
|
+
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
28
|
+
'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
|
|
29
|
+
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
30
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
51
31
|
'',
|
|
52
32
|
].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
33
|
}
|
|
61
34
|
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
|
-
}
|
|
35
|
+
cleanupQoderLegacy(options);
|
|
36
|
+
writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md'));
|
|
80
37
|
}
|
|
81
38
|
export function installKiro(options) {
|
|
82
|
-
|
|
39
|
+
cleanupKiroLegacy(options);
|
|
83
40
|
writeTextFile(path.join(rootForScope(options, '.kiro'), 'steering', 'openturtle-cli.md'), [
|
|
84
41
|
'# OpenTurtle CLI',
|
|
85
42
|
'',
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
43
|
+
'Run OpenTurtle operations with the direct `ot ... --json` CLI. Do not use an OpenTurtle MCP server or install OpenTurtle hooks.',
|
|
44
|
+
'Start with `command -v ot` and `ot --json doctor`; if authentication is missing, ask the user to run `ot auth login --web`.',
|
|
45
|
+
'Before a write, run the same command with `--dry-run` and inspect the request path and body.',
|
|
46
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
89
47
|
'',
|
|
90
48
|
].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
49
|
}
|
|
109
50
|
export function uninstallClaude(options) {
|
|
51
|
+
cleanupClaudeLegacy(options);
|
|
110
52
|
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
53
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
121
54
|
}
|
|
122
55
|
export function uninstallCodex(options) {
|
|
56
|
+
cleanupCodexLegacy(options);
|
|
123
57
|
const root = rootForScope(options, '.codex');
|
|
124
|
-
removeCodexManagedConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
|
|
125
58
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
126
|
-
|
|
59
|
+
if (options.scope === 'project')
|
|
60
|
+
removeAgentsSnippet(options.workspace);
|
|
127
61
|
}
|
|
128
62
|
export function uninstallCursor(options) {
|
|
129
|
-
|
|
63
|
+
cleanupCursorLegacy(options);
|
|
130
64
|
const root = path.join(options.workspace, '.cursor');
|
|
131
65
|
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
66
|
}
|
|
138
67
|
export function uninstallQoder(options) {
|
|
68
|
+
cleanupQoderLegacy(options);
|
|
139
69
|
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
70
|
removeDirIfExists(path.join(root, 'skills', 'openturtle-cli'), root);
|
|
71
|
+
}
|
|
72
|
+
export function uninstallKiro(options) {
|
|
73
|
+
cleanupKiroLegacy(options);
|
|
74
|
+
const root = rootForScope(options, '.kiro');
|
|
75
|
+
removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
|
|
76
|
+
}
|
|
77
|
+
function cleanupClaudeLegacy(options) {
|
|
78
|
+
const root = rootForScope(options, '.claude');
|
|
79
|
+
removeLegacyJsonServer(claudeLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
|
|
80
|
+
const settingsPath = claudeSettingsPath(options.scope, options.workspace, options.homeDir);
|
|
81
|
+
if (!fs.existsSync(settingsPath))
|
|
82
|
+
return;
|
|
83
|
+
const settings = readJsonFile(settingsPath, {});
|
|
84
|
+
if (settings.mcpServers && typeof settings.mcpServers === 'object')
|
|
85
|
+
delete settings.mcpServers['ot-cli'];
|
|
86
|
+
writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
|
|
87
|
+
}
|
|
88
|
+
function cleanupCodexLegacy(options) {
|
|
89
|
+
const root = rootForScope(options, '.codex');
|
|
90
|
+
removeLegacyCodexConfig(codexConfigPath(options.scope, options.workspace, options.homeDir), root);
|
|
91
|
+
}
|
|
92
|
+
function cleanupCursorLegacy(options) {
|
|
93
|
+
removeLegacyJsonServer(cursorLegacyServerPath(options.workspace), options.workspace);
|
|
94
|
+
const hooksPath = path.join(options.homeDir || os.homedir(), '.cursor', 'hooks.json');
|
|
95
|
+
if (!fs.existsSync(hooksPath))
|
|
96
|
+
return;
|
|
97
|
+
const hooks = readJsonFile(hooksPath, {});
|
|
98
|
+
writeJsonOrRemoveEmpty(hooksPath, removeCursorHookEntries(hooks), path.dirname(hooksPath));
|
|
99
|
+
}
|
|
100
|
+
function cleanupQoderLegacy(options) {
|
|
101
|
+
const root = rootForScope(options, '.qoder');
|
|
102
|
+
const mcpPath = qoderLegacyServerPath(options.scope, options.workspace, options.homeDir);
|
|
103
|
+
removeLegacyJsonServer(mcpPath, options.scope === 'user' ? path.dirname(mcpPath) : options.workspace);
|
|
144
104
|
const settingsPath = options.scope === 'user'
|
|
145
105
|
? path.join(options.homeDir || os.homedir(), '.qoder', 'settings.json')
|
|
146
106
|
: path.join(options.workspace, '.qoder', 'settings.local.json');
|
|
147
|
-
if (fs.existsSync(settingsPath))
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
107
|
+
if (!fs.existsSync(settingsPath))
|
|
108
|
+
return;
|
|
109
|
+
const settings = readJsonFile(settingsPath, {});
|
|
110
|
+
writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
|
|
151
111
|
}
|
|
152
|
-
|
|
112
|
+
function cleanupKiroLegacy(options) {
|
|
153
113
|
const root = rootForScope(options, '.kiro');
|
|
154
|
-
|
|
155
|
-
removeFileIfExists(path.join(root, 'steering', 'openturtle-cli.md'), root);
|
|
114
|
+
removeLegacyJsonServer(kiroLegacyServerPath(options.scope, options.workspace, options.homeDir), root);
|
|
156
115
|
removeFileIfExists(path.join(root, 'agents', 'openturtle-cli.json'), root);
|
|
157
116
|
removeFileIfExists(path.join(root, 'hooks', 'openturtle-cli-audit.kiro.hook'), root);
|
|
158
117
|
}
|
|
@@ -161,108 +120,26 @@ function rootForScope(options, dirName) {
|
|
|
161
120
|
? path.join(options.homeDir || os.homedir(), dirName)
|
|
162
121
|
: path.join(options.workspace, dirName);
|
|
163
122
|
}
|
|
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 });
|
|
123
|
+
function writeSkill(filePath) {
|
|
124
|
+
writeTextFile(filePath, skillContent());
|
|
171
125
|
}
|
|
172
|
-
function
|
|
173
|
-
return
|
|
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];
|
|
206
|
-
}
|
|
207
|
-
function codexHookBlock(hook, target, event) {
|
|
208
|
-
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
|
-
'---',
|
|
218
|
-
'name: openturtle-cli',
|
|
219
|
-
'description: Use OpenTurtle projects, team roster, project context, knowledge, meetings, goals, todos, and worklogs from an agent or terminal without requiring the Desktop client.',
|
|
220
|
-
'---',
|
|
221
|
-
'',
|
|
222
|
-
'# OpenTurtle CLI',
|
|
223
|
-
'',
|
|
224
|
-
`Use the \`ot-cli\` MCP server in ${agentName} when its tool covers the task. Use the direct \`ot\` command for projects, roster, project context, meetings, or any newer operation that is not exposed by MCP.`,
|
|
225
|
-
'If authentication is missing, ask the user to run `ot auth login --web`; never request or expose their browser token.',
|
|
226
|
-
'Run `ot --json doctor` before a remote workflow when authentication or server reachability is uncertain.',
|
|
227
|
-
'Read commands emit JSON. Before a write, run the same command with `--dry-run` and inspect `request.path` and `request.body`.',
|
|
228
|
-
'Treat knowledge as reusable memory objects. Treat source files as original materials referenced by knowledge, not as a second knowledge system.',
|
|
229
|
-
'',
|
|
230
|
-
'## Meeting workflow',
|
|
231
|
-
'',
|
|
232
|
-
'1. Resolve the project with `ot projects resolve`, then inspect `ot roster list` and `ot context get`.',
|
|
233
|
-
'2. Upload audio with `ot meetings upload` or inspect an existing meeting with `ot meetings show`, `transcript`, and `minutes`.',
|
|
234
|
-
'3. Publish complete minutes with `ot meetings publish <meeting-id>`. Meeting minutes belong in Knowledge, not WorkLog.',
|
|
235
|
-
'4. Review drafts using `ot meetings updates list <meeting-id>` and edit them when needed.',
|
|
236
|
-
'5. Confirm only reviewed updates with `ot meetings updates confirm ... --knowledge <knowledge-id>`. Never confirm without the saved meeting knowledge id.',
|
|
237
|
-
'6. Use the materialized Todo or WorkLog ids returned by confirmation as the project update evidence.',
|
|
238
|
-
'',
|
|
239
|
-
'## Todo quality',
|
|
240
|
-
'',
|
|
241
|
-
'When creating or proposing OpenTurtle todos through any available OpenTurtle MCP tool, include source context, scope/environment, concrete action steps, measurable acceptance criteria, and required evidence.',
|
|
242
|
-
'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 the user first. Use AskUserQuestion when the host supports it; otherwise ask one concise question before creating the todo.',
|
|
244
|
-
'Avoid vague acceptance such as "no obvious delay", "works normally", or "verify it". Prefer concrete thresholds, environments, and evidence.',
|
|
245
|
-
'Use `--body-file <json>` or `--body-file -` for structured writes so shell quoting does not corrupt payloads.',
|
|
246
|
-
'Do not write to `.openturtle/`; CLI state lives in `.openturtle-cli/` and `~/.openturtle/cli/`.',
|
|
247
|
-
'',
|
|
248
|
-
].join('\n'));
|
|
126
|
+
export function skillContent() {
|
|
127
|
+
return fs.readFileSync(fileURLToPath(new URL('../../skill/openturtle-cli/SKILL.md', import.meta.url)), 'utf8');
|
|
249
128
|
}
|
|
250
129
|
function appendAgentsSnippet(workspace) {
|
|
251
130
|
const filePath = path.join(workspace, 'AGENTS.md');
|
|
252
131
|
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
253
|
-
const
|
|
254
|
-
if (existing.includes(marker))
|
|
255
|
-
return;
|
|
132
|
+
const withoutOld = existing.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
|
|
256
133
|
const snippet = [
|
|
257
|
-
|
|
134
|
+
'<!-- OPENTURTLE-CLI -->',
|
|
258
135
|
'',
|
|
259
|
-
'
|
|
260
|
-
'
|
|
261
|
-
'Todo quality:
|
|
136
|
+
'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.',
|
|
137
|
+
'Run `ot --json doctor` when authentication or server reachability is uncertain, and use `--dry-run` before writes.',
|
|
138
|
+
'Todo quality: include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. Ask one concise question when required information is unclear.',
|
|
262
139
|
'',
|
|
263
140
|
'<!-- /OPENTURTLE-CLI -->',
|
|
264
141
|
].join('\n');
|
|
265
|
-
writeTextFile(filePath, `${
|
|
142
|
+
writeTextFile(filePath, `${withoutOld}${withoutOld ? '\n\n' : ''}${snippet}\n`);
|
|
266
143
|
}
|
|
267
144
|
function removeAgentsSnippet(workspace) {
|
|
268
145
|
const filePath = path.join(workspace, 'AGENTS.md');
|
|
@@ -270,32 +147,12 @@ function removeAgentsSnippet(workspace) {
|
|
|
270
147
|
return;
|
|
271
148
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
272
149
|
const cleaned = content.replace(/<!-- OPENTURTLE-CLI -->[\s\S]*?<!-- \/OPENTURTLE-CLI -->\n?/m, '').trimEnd();
|
|
273
|
-
if (cleaned)
|
|
150
|
+
if (cleaned)
|
|
274
151
|
fs.writeFileSync(filePath, `${cleaned}\n`, 'utf-8');
|
|
275
|
-
|
|
276
|
-
else {
|
|
152
|
+
else
|
|
277
153
|
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
154
|
}
|
|
295
155
|
function writeTextFile(filePath, content) {
|
|
296
156
|
ensureFileDir(filePath);
|
|
297
157
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
298
158
|
}
|
|
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
|
+
}
|