@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/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 { installGitHook, uninstallGitHook } from './installers/git.js';
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, Boolean(options.withAgentHooks));
130
+ installTarget(target, workspace, options.scope);
134
131
  }
135
132
  print({
136
133
  installed: options.target,
137
- mcp_server_name: 'ot-cli',
138
- with_agent_hooks: Boolean(options.withAgentHooks),
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('mcp [subcommand]')
343
- .description('MCP server commands')
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, withAgentHooks) {
331
+ function installTarget(target, workspace, scope) {
388
332
  if (target === 'git')
389
- installGitHook(workspace);
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({ workspace, scope, withAgentHooks });
336
+ installClaude(options);
392
337
  if (target === 'codex')
393
- installCodex({ workspace, scope, withAgentHooks });
338
+ installCodex(options);
394
339
  if (target === 'cursor')
395
- installCursor({ workspace, scope, withAgentHooks });
340
+ installCursor(options);
396
341
  if (target === 'qoder')
397
- installQoder({ workspace, scope, withAgentHooks });
342
+ installQoder(options);
398
343
  if (target === 'kiro')
399
- installKiro({ workspace, scope, withAgentHooks });
344
+ installKiro(options);
345
+ recordInstallation({ target, scope, workspace });
400
346
  }
401
347
  function uninstallTarget(target, workspace, scope) {
402
- const options = { workspace, scope, withAgentHooks: true };
348
+ const options = { workspace, scope };
403
349
  if (target === 'git')
404
- uninstallGitHook(workspace);
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 ['git', 'claude', 'codex', 'cursor', 'qoder', 'kiro'];
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 { hookCommand } from '../commands/hook.js';
5
- import { readJsonFile, writeJsonFile } from '../core/json.js';
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
- mergeJsonMcpServer(claudeMcpPath(options.scope, options.workspace, options.homeDir));
11
- writeSkill(path.join(rootForScope(options, '.claude'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Claude');
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
- const configPath = codexConfigPath(options.scope, options.workspace, options.homeDir);
29
- mergeCodexMcpServer(configPath);
30
- writeSkill(path.join(rootForScope(options, '.codex'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Codex');
31
- appendAgentsSnippet(options.workspace);
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
- mergeJsonMcpServer(cursorMcpPath(options.workspace));
19
+ cleanupCursorLegacy(options);
42
20
  writeTextFile(path.join(options.workspace, '.cursor', 'rules', 'openturtle-cli.mdc'), [
43
21
  '---',
44
- 'description: Use OpenTurtle CLI MCP tools for goals, todos, reusable knowledge, source files, worklogs, and report drafts.',
22
+ 'description: Use the OpenTurtle CLI directly for projects, goals, todos, knowledge, meetings, and worklogs.',
45
23
  'alwaysApply: true',
46
24
  '---',
47
25
  '',
48
- 'Prefer the `ot-cli` MCP server when reading or updating OpenTurtle goals, todos, worklogs, reports, or reusable workspace knowledge.',
49
- 'Use knowledge source-file tools only when you need the original files behind a knowledge object.',
50
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
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
- mergeJsonMcpServer(qoderMcpPath(options.scope, options.workspace, options.homeDir));
63
- writeSkill(path.join(rootForScope(options, '.qoder'), 'skills', 'openturtle-cli', 'SKILL.md'), 'Qoder');
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
- mergeJsonMcpServer(kiroMcpPath(options.scope, options.workspace, options.homeDir));
38
+ cleanupKiroLegacy(options);
83
39
  writeTextFile(path.join(rootForScope(options, '.kiro'), 'steering', 'openturtle-cli.md'), [
84
40
  '# OpenTurtle CLI',
85
41
  '',
86
- 'Use the `ot-cli` MCP server for OpenTurtle goals, todos, reusable knowledge, source files, worklogs, and daily report drafts.',
87
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
88
- 'Hooks installed by OpenTurtle are audit-only and must not block the user workflow.',
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
- removeAgentsSnippet(options.workspace);
58
+ if (options.scope === 'project')
59
+ removeAgentsSnippet(options.workspace);
127
60
  }
128
61
  export function uninstallCursor(options) {
129
- removeJsonMcpServer(cursorMcpPath(options.workspace), 'ot-cli', options.workspace);
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
- const settings = readJsonFile(settingsPath, {});
149
- writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
150
- }
106
+ if (!fs.existsSync(settingsPath))
107
+ return;
108
+ const settings = readJsonFile(settingsPath, {});
109
+ writeJsonOrRemoveEmpty(settingsPath, removeManagedHookEntries(settings), root);
151
110
  }
152
- export function uninstallKiro(options) {
111
+ function cleanupKiroLegacy(options) {
153
112
  const root = rootForScope(options, '.kiro');
154
- removeJsonMcpServer(kiroMcpPath(options.scope, options.workspace, options.homeDir), 'ot-cli', root);
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 mergeJsonMcpServerInSettings(settingsPath) {
165
- const settings = readJsonFile(settingsPath, {});
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 codexHookBlock(hook, target, event) {
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
- `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.`,
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 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`.',
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 through any available OpenTurtle MCP tool, include source context, scope/environment, concrete action steps, measurable acceptance criteria, and required evidence.',
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 the user first. Use AskUserQuestion when the host supports it; otherwise ask one concise question before creating the todo.',
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 marker = '<!-- OPENTURTLE-CLI -->';
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
- marker,
166
+ '<!-- OPENTURTLE-CLI -->',
258
167
  '',
259
- 'When OpenTurtle goals, todos, reusable knowledge, source files, worklogs, or report drafts are needed, prefer the `ot-cli` MCP server.',
260
- 'Knowledge means reusable memory objects; source files are original materials behind knowledge.',
261
- 'Todo quality: when creating or proposing todos, include source context, scope/environment, concrete steps, measurable acceptance criteria, and evidence. If any of those are unclear, use AskUserQuestion when available or ask the user one concise question before creating the todo.',
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, `${existing.trimEnd()}${existing.trim() ? '\n\n' : ''}${snippet}\n`);
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.2",
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"