@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.
@@ -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
- mcp_server_name: 'ot-cli',
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()) {
@@ -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
- return { current: currentVersion, latest, available: true, updated: true };
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.OPENTURTLE_DISABLE_AUTO_UPDATE === '1' || env.OPENTURTLE_AUTO_UPDATE_WORKER === '1')
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
- import { ensureFileDir } from './paths.js';
6
- export const MCP_SERVER_NAME = 'ot-cli';
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[name];
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 claudeMcpPath(scope, workspace, homeDir = os.homedir()) {
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 cursorMcpPath(workspace) {
37
+ export function cursorLegacyServerPath(workspace) {
50
38
  return path.join(workspace, '.mcp.json');
51
39
  }
52
- export function qoderMcpPath(scope, workspace, homeDir = os.homedir()) {
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 kiroMcpPath(scope, workspace, homeDir = os.homedir()) {
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 mergeCodexMcpServer(filePath) {
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.${MCP_SERVER_NAME}`);
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
  }
@@ -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
- mcpServerName: 'ot-cli',
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 { ingestRawHook, recordGitPostCommit } from './commands/hook.js';
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 { installGitHook, uninstallGitHook } from './installers/git.js';
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, Boolean(options.withAgentHooks));
131
+ installTarget(target, workspace, options.scope);
134
132
  }
135
133
  print({
136
134
  installed: options.target,
137
- mcp_server_name: 'ot-cli',
138
- with_agent_hooks: Boolean(options.withAgentHooks),
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('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
- }));
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, withAgentHooks) {
336
+ function installTarget(target, workspace, scope) {
388
337
  if (target === 'git')
389
- installGitHook(workspace);
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({ workspace, scope, withAgentHooks });
341
+ installClaude(options);
392
342
  if (target === 'codex')
393
- installCodex({ workspace, scope, withAgentHooks });
343
+ installCodex(options);
394
344
  if (target === 'cursor')
395
- installCursor({ workspace, scope, withAgentHooks });
345
+ installCursor(options);
396
346
  if (target === 'qoder')
397
- installQoder({ workspace, scope, withAgentHooks });
347
+ installQoder(options);
398
348
  if (target === 'kiro')
399
- installKiro({ workspace, scope, withAgentHooks });
349
+ installKiro(options);
350
+ recordInstallation({ target, scope, workspace });
400
351
  }
401
352
  function uninstallTarget(target, workspace, scope) {
402
- const options = { workspace, scope, withAgentHooks: true };
353
+ const options = { workspace, scope };
403
354
  if (target === 'git')
404
- uninstallGitHook(workspace);
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 ['git', 'claude', 'codex', 'cursor', 'qoder', 'kiro'];
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) {