@openturtle/cli 0.3.0
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 +674 -0
- package/dist/commands/collaboration.js +315 -0
- package/dist/commands/hook.js +162 -0
- package/dist/commands/resources.js +118 -0
- package/dist/core/api-client.js +221 -0
- package/dist/core/auth.js +35 -0
- package/dist/core/defaults.js +1 -0
- package/dist/core/input.js +37 -0
- package/dist/core/json.js +34 -0
- package/dist/core/managed-block.js +25 -0
- package/dist/core/mcp-config.js +129 -0
- package/dist/core/paths.js +32 -0
- package/dist/core/store.js +102 -0
- package/dist/core/types.js +1 -0
- package/dist/core/version.js +33 -0
- package/dist/core/web-auth.js +115 -0
- package/dist/index.js +469 -0
- package/dist/installers/agents.js +301 -0
- package/dist/installers/git.js +63 -0
- package/dist/installers/uninstall.js +107 -0
- package/dist/mcp/server.js +185 -0
- package/dist/watchers/importers.js +69 -0
- package/package.json +48 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { ApiClient } from './api-client.js';
|
|
3
|
+
import { readAuth, writeAuth } from './auth.js';
|
|
4
|
+
import { DEFAULT_SERVER_URL } from './defaults.js';
|
|
5
|
+
const DEFAULT_LOCAL_WEB_PORT = '5174';
|
|
6
|
+
export async function loginWithWeb(options, dependencies = {}) {
|
|
7
|
+
const current = readAuth(options.homeDir);
|
|
8
|
+
const server = normalizeServer(options.server || current.server || DEFAULT_SERVER_URL);
|
|
9
|
+
const verificationUri = resolveWebLoginUrl(server, options.webUrl);
|
|
10
|
+
const client = dependencies.client || new ApiClient({ server, homeDir: options.homeDir });
|
|
11
|
+
const sleep = dependencies.sleep || ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
|
|
12
|
+
const now = dependencies.now || Date.now;
|
|
13
|
+
const open = dependencies.openBrowser || openBrowser;
|
|
14
|
+
const device = await client.post('/api/auth/cli/device', {
|
|
15
|
+
client_name: 'OpenTurtle CLI',
|
|
16
|
+
verification_uri: verificationUri,
|
|
17
|
+
});
|
|
18
|
+
dependencies.onDeviceCreated?.(device);
|
|
19
|
+
const browserOpened = options.noOpen ? false : await open(device.verification_uri_complete);
|
|
20
|
+
const expiresAt = now() + device.expires_in * 1000;
|
|
21
|
+
let intervalSeconds = Math.max(1, device.interval || 2);
|
|
22
|
+
while (now() < expiresAt) {
|
|
23
|
+
const result = await client.post('/api/auth/cli/token', {
|
|
24
|
+
device_code: device.device_code,
|
|
25
|
+
});
|
|
26
|
+
if (result.status === 'authorization_pending') {
|
|
27
|
+
intervalSeconds = Math.max(1, result.interval || intervalSeconds);
|
|
28
|
+
await sleep(intervalSeconds * 1000);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const token = result.access_token || '';
|
|
32
|
+
const refreshToken = result.refresh_token || '';
|
|
33
|
+
if (!token || !refreshToken) {
|
|
34
|
+
throw new Error('Web login response did not include access and refresh tokens');
|
|
35
|
+
}
|
|
36
|
+
writeAuth({
|
|
37
|
+
server,
|
|
38
|
+
token,
|
|
39
|
+
refreshToken,
|
|
40
|
+
username: result.username,
|
|
41
|
+
teamId: result.team_id,
|
|
42
|
+
}, options.homeDir);
|
|
43
|
+
return {
|
|
44
|
+
logged_in: true,
|
|
45
|
+
server,
|
|
46
|
+
username: result.username || null,
|
|
47
|
+
team_id: result.team_id || null,
|
|
48
|
+
browser_opened: browserOpened,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
throw new Error('Web login expired before authorization completed');
|
|
52
|
+
}
|
|
53
|
+
export function resolveWebLoginUrl(server, override) {
|
|
54
|
+
if (override)
|
|
55
|
+
return normalizeWebUrl(override);
|
|
56
|
+
const apiUrl = new URL(normalizeServer(server));
|
|
57
|
+
if (apiUrl.hostname === '127.0.0.1' || apiUrl.hostname === 'localhost' || apiUrl.hostname === '::1') {
|
|
58
|
+
apiUrl.port = DEFAULT_LOCAL_WEB_PORT;
|
|
59
|
+
apiUrl.pathname = '/cli-auth';
|
|
60
|
+
apiUrl.search = '';
|
|
61
|
+
apiUrl.hash = '';
|
|
62
|
+
return apiUrl.toString();
|
|
63
|
+
}
|
|
64
|
+
apiUrl.pathname = `${apiUrl.pathname.replace(/\/$/, '')}/cli-auth`.replace(/\/+/g, '/');
|
|
65
|
+
apiUrl.search = '';
|
|
66
|
+
apiUrl.hash = '';
|
|
67
|
+
return apiUrl.toString();
|
|
68
|
+
}
|
|
69
|
+
export async function openBrowser(url, platform = process.platform, spawnBrowser = spawn) {
|
|
70
|
+
const launch = browserLaunchCommand(url, platform);
|
|
71
|
+
if (!launch)
|
|
72
|
+
return false;
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
let child;
|
|
75
|
+
try {
|
|
76
|
+
child = spawnBrowser(launch.command, launch.args, {
|
|
77
|
+
detached: true,
|
|
78
|
+
stdio: 'ignore',
|
|
79
|
+
windowsHide: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
resolve(false);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
child.once('spawn', () => {
|
|
87
|
+
child.unref();
|
|
88
|
+
resolve(true);
|
|
89
|
+
});
|
|
90
|
+
child.once('error', () => resolve(false));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export function browserLaunchCommand(url, platform = process.platform) {
|
|
94
|
+
if (platform === 'darwin')
|
|
95
|
+
return { command: 'open', args: [url] };
|
|
96
|
+
if (platform === 'linux')
|
|
97
|
+
return { command: 'xdg-open', args: [url] };
|
|
98
|
+
if (platform === 'win32')
|
|
99
|
+
return { command: 'cmd', args: ['/c', 'start', '', url] };
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function normalizeServer(value) {
|
|
103
|
+
const url = new URL(value);
|
|
104
|
+
url.pathname = url.pathname.replace(/\/$/, '') || '/';
|
|
105
|
+
url.search = '';
|
|
106
|
+
url.hash = '';
|
|
107
|
+
return url.toString().replace(/\/$/, '');
|
|
108
|
+
}
|
|
109
|
+
function normalizeWebUrl(value) {
|
|
110
|
+
const url = new URL(value);
|
|
111
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
112
|
+
throw new Error('--web-url must use http or https');
|
|
113
|
+
}
|
|
114
|
+
return url.toString();
|
|
115
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { registerCollaborationCommands } from './commands/collaboration.js';
|
|
5
|
+
import { ingestRawHook, recordGitPostCommit } from './commands/hook.js';
|
|
6
|
+
import { doctor, draftDailyReport, createResourceCommands } from './commands/resources.js';
|
|
7
|
+
import { ApiClient, ApiError } from './core/api-client.js';
|
|
8
|
+
import { clearAuth, readAuth, redactToken, writeAuth } from './core/auth.js';
|
|
9
|
+
import { DEFAULT_SERVER_URL } from './core/defaults.js';
|
|
10
|
+
import { findWorkspaceRoot } from './core/paths.js';
|
|
11
|
+
import { initProjectStore } from './core/store.js';
|
|
12
|
+
import { cliVersion } from './core/version.js';
|
|
13
|
+
import { loginWithWeb } from './core/web-auth.js';
|
|
14
|
+
import { installClaude, installCodex, installCursor, installKiro, installQoder, uninstallClaude, uninstallCodex, uninstallCursor, uninstallKiro, uninstallQoder, } from './installers/agents.js';
|
|
15
|
+
import { installGitHook, uninstallGitHook } from './installers/git.js';
|
|
16
|
+
import { uninstallAllLocalState } from './installers/uninstall.js';
|
|
17
|
+
import { serveMcp } from './mcp/server.js';
|
|
18
|
+
import { candidateHistoryPaths, importHistory } from './watchers/importers.js';
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name('ot')
|
|
22
|
+
.description('OpenTurtle collaboration, knowledge, meeting, and local agent CLI')
|
|
23
|
+
.version(cliVersion());
|
|
24
|
+
program.option('-j, --json', 'emit machine-readable JSON, including errors');
|
|
25
|
+
program
|
|
26
|
+
.command('init')
|
|
27
|
+
.option('--update-gitignore', 'append .openturtle-cli to .gitignore')
|
|
28
|
+
.action(action((options) => {
|
|
29
|
+
const workspace = findWorkspaceRoot();
|
|
30
|
+
const store = initProjectStore(workspace);
|
|
31
|
+
if (options.updateGitignore)
|
|
32
|
+
updateGitignore(workspace);
|
|
33
|
+
print({ initialized: true, project_dir: store.dir });
|
|
34
|
+
}));
|
|
35
|
+
program
|
|
36
|
+
.command('doctor')
|
|
37
|
+
.option('--target <target>')
|
|
38
|
+
.option('--json')
|
|
39
|
+
.action(action(async (options) => {
|
|
40
|
+
const result = doctor(findWorkspaceRoot(), options.target);
|
|
41
|
+
const api = await new ApiClient().probe();
|
|
42
|
+
print({ ...result, api }, options.json);
|
|
43
|
+
}));
|
|
44
|
+
const auth = program.command('auth');
|
|
45
|
+
auth
|
|
46
|
+
.command('login')
|
|
47
|
+
.option('--server <url>')
|
|
48
|
+
.option('--token <jwt>')
|
|
49
|
+
.option('--username <name>')
|
|
50
|
+
.option('--password <password>')
|
|
51
|
+
.option('--web', 'log in through OpenTurtle Web Admin')
|
|
52
|
+
.option('--web-url <url>', 'override the Web authorization page URL')
|
|
53
|
+
.option('--no-open', 'print the authorization URL without opening a browser')
|
|
54
|
+
.action(action(async (options) => {
|
|
55
|
+
if (options.web) {
|
|
56
|
+
if (options.token || options.username || options.password) {
|
|
57
|
+
throw new Error('--web cannot be combined with token or password login');
|
|
58
|
+
}
|
|
59
|
+
const result = await loginWithWeb({
|
|
60
|
+
server: options.server,
|
|
61
|
+
webUrl: options.webUrl,
|
|
62
|
+
noOpen: options.open === false,
|
|
63
|
+
}, {
|
|
64
|
+
onDeviceCreated: (session) => {
|
|
65
|
+
process.stderr.write(`Open ${session.verification_uri_complete}\n`);
|
|
66
|
+
process.stderr.write(`Device code: ${session.user_code}\n`);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
print(result);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (options.webUrl || options.open === false) {
|
|
73
|
+
throw new Error('--web-url and --no-open require --web');
|
|
74
|
+
}
|
|
75
|
+
const server = options.server || DEFAULT_SERVER_URL;
|
|
76
|
+
if (options.token) {
|
|
77
|
+
writeAuth({ server, token: options.token, username: options.username });
|
|
78
|
+
print({ logged_in: true, server, token: redactToken(options.token) });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!options.username || !options.password) {
|
|
82
|
+
throw new Error('Use --token or provide --username and --password');
|
|
83
|
+
}
|
|
84
|
+
const client = new ApiClient({ server });
|
|
85
|
+
const result = await client.post('/api/auth/login', {
|
|
86
|
+
username: options.username,
|
|
87
|
+
password: options.password,
|
|
88
|
+
});
|
|
89
|
+
const token = String(result.access_token || result.token || '');
|
|
90
|
+
if (!token)
|
|
91
|
+
throw new Error('Login response did not include token');
|
|
92
|
+
const refreshToken = String(result.refresh_token || '');
|
|
93
|
+
writeAuth({
|
|
94
|
+
server,
|
|
95
|
+
token,
|
|
96
|
+
refreshToken: refreshToken || undefined,
|
|
97
|
+
username: options.username,
|
|
98
|
+
});
|
|
99
|
+
print({ logged_in: true, server, token: redactToken(token) });
|
|
100
|
+
}));
|
|
101
|
+
auth.command('status').action(action(() => {
|
|
102
|
+
const current = readAuth();
|
|
103
|
+
print({
|
|
104
|
+
server: current.server || null,
|
|
105
|
+
username: current.username || null,
|
|
106
|
+
team_id: current.teamId || null,
|
|
107
|
+
token: redactToken(current.token),
|
|
108
|
+
has_refresh_token: Boolean(current.refreshToken),
|
|
109
|
+
logged_in: Boolean(current.token),
|
|
110
|
+
});
|
|
111
|
+
}));
|
|
112
|
+
auth.command('logout').action(action(() => {
|
|
113
|
+
clearAuth();
|
|
114
|
+
print({ logged_out: true });
|
|
115
|
+
}));
|
|
116
|
+
program
|
|
117
|
+
.command('install')
|
|
118
|
+
.option('--target <target>')
|
|
119
|
+
.option('--scope <scope>', 'project|user', 'project')
|
|
120
|
+
.option('--with-agent-hooks')
|
|
121
|
+
.action(action((options) => {
|
|
122
|
+
requireOption(options.target, '--target');
|
|
123
|
+
const workspace = findWorkspaceRoot();
|
|
124
|
+
for (const target of expandTargets(options.target)) {
|
|
125
|
+
installTarget(target, workspace, options.scope, Boolean(options.withAgentHooks));
|
|
126
|
+
}
|
|
127
|
+
print({
|
|
128
|
+
installed: options.target,
|
|
129
|
+
mcp_server_name: 'ot-cli',
|
|
130
|
+
with_agent_hooks: Boolean(options.withAgentHooks),
|
|
131
|
+
});
|
|
132
|
+
}));
|
|
133
|
+
program
|
|
134
|
+
.command('uninstall')
|
|
135
|
+
.option('--target <target>')
|
|
136
|
+
.option('--scope <scope>', 'project|user', 'project')
|
|
137
|
+
.action(action((options) => {
|
|
138
|
+
requireOption(options.target, '--target');
|
|
139
|
+
const workspace = findWorkspaceRoot();
|
|
140
|
+
for (const target of expandTargets(options.target)) {
|
|
141
|
+
uninstallTarget(target, workspace, options.scope);
|
|
142
|
+
}
|
|
143
|
+
const local_state = options.target === 'all' ? uninstallAllLocalState(workspace) : { removedProjectDir: false };
|
|
144
|
+
print({ uninstalled: options.target, scope: options.scope, local_state });
|
|
145
|
+
}));
|
|
146
|
+
const hook = program.command('hook');
|
|
147
|
+
hook
|
|
148
|
+
.command('ingest')
|
|
149
|
+
.option('--target <target>')
|
|
150
|
+
.option('--event <event>')
|
|
151
|
+
.option('--stdin-json')
|
|
152
|
+
.action(action(async (options) => {
|
|
153
|
+
requireOption(options.target, '--target');
|
|
154
|
+
requireOption(options.event, '--event');
|
|
155
|
+
const stdin = options.stdinJson ? await readStdin() : '{}';
|
|
156
|
+
const result = ingestRawHook({
|
|
157
|
+
target: options.target,
|
|
158
|
+
event: options.event,
|
|
159
|
+
stdin,
|
|
160
|
+
workspace: findWorkspaceRoot(),
|
|
161
|
+
});
|
|
162
|
+
if (!result.ok)
|
|
163
|
+
process.exitCode = 0;
|
|
164
|
+
}));
|
|
165
|
+
hook.command('git-post-commit').action(action(() => {
|
|
166
|
+
recordGitPostCommit(findWorkspaceRoot());
|
|
167
|
+
}));
|
|
168
|
+
hook.command('doctor').action(action(() => print(doctor(findWorkspaceRoot()))));
|
|
169
|
+
program
|
|
170
|
+
.command('import-history')
|
|
171
|
+
.option('--target <target>')
|
|
172
|
+
.action(action((options) => {
|
|
173
|
+
requireOption(options.target, '--target');
|
|
174
|
+
const workspace = findWorkspaceRoot();
|
|
175
|
+
if (options.target === 'all') {
|
|
176
|
+
print(['claude', 'codex', 'cursor', 'qoder', 'kiro'].map((target) => importHistory(target, workspace)));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
print(importHistory(options.target, workspace));
|
|
180
|
+
}));
|
|
181
|
+
program
|
|
182
|
+
.command('watch')
|
|
183
|
+
.option('--target <target>')
|
|
184
|
+
.action(action((options) => {
|
|
185
|
+
requireOption(options.target, '--target');
|
|
186
|
+
print({
|
|
187
|
+
target: options.target,
|
|
188
|
+
watcher_paths: candidateHistoryPaths(options.target),
|
|
189
|
+
note: 'v1 watch uses import-history polling semantics; native hooks are preferred where supported.',
|
|
190
|
+
});
|
|
191
|
+
}));
|
|
192
|
+
const resources = createResourceCommands();
|
|
193
|
+
const goals = program.command('goals');
|
|
194
|
+
goals
|
|
195
|
+
.command('list')
|
|
196
|
+
.option('--project <id>')
|
|
197
|
+
.option('--mine')
|
|
198
|
+
.option('--status <status>')
|
|
199
|
+
.option('--json')
|
|
200
|
+
.action(action(async (options) => print(await resources.listGoals({ project_id: options.project, mine: options.mine, status: options.status }), options.json)));
|
|
201
|
+
goals
|
|
202
|
+
.command('show <goalId>')
|
|
203
|
+
.option('--json')
|
|
204
|
+
.action(action(async (goalId, options) => print(await resources.showGoal(goalId), options.json)));
|
|
205
|
+
const todos = program.command('todos');
|
|
206
|
+
todos
|
|
207
|
+
.command('list')
|
|
208
|
+
.option('--project <id>')
|
|
209
|
+
.option('--mine')
|
|
210
|
+
.option('--status <status>')
|
|
211
|
+
.option('--json')
|
|
212
|
+
.action(action(async (options) => print(await resources.listTodos({ project_id: options.project, mine: options.mine, status: options.status }), options.json)));
|
|
213
|
+
todos
|
|
214
|
+
.command('show <todoId>')
|
|
215
|
+
.option('--json')
|
|
216
|
+
.action(action(async (todoId, options) => print(await resources.showTodo(todoId), options.json)));
|
|
217
|
+
todos
|
|
218
|
+
.command('status <todoId> <status>')
|
|
219
|
+
.option('--dry-run')
|
|
220
|
+
.action(action(async (todoId, status, options) => {
|
|
221
|
+
if (options.dryRun) {
|
|
222
|
+
print({
|
|
223
|
+
dry_run: true,
|
|
224
|
+
request: {
|
|
225
|
+
method: 'PATCH',
|
|
226
|
+
path: `/api/todos/${encodeURIComponent(todoId)}/status`,
|
|
227
|
+
body: { status },
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
print(await resources.updateTodoStatus(todoId, status));
|
|
233
|
+
}));
|
|
234
|
+
todos
|
|
235
|
+
.command('progress <todoId>')
|
|
236
|
+
.option('--message <text>')
|
|
237
|
+
.option('--dry-run')
|
|
238
|
+
.action(action(async (todoId, options) => {
|
|
239
|
+
requireOption(options.message, '--message');
|
|
240
|
+
if (options.dryRun) {
|
|
241
|
+
print({
|
|
242
|
+
dry_run: true,
|
|
243
|
+
request: {
|
|
244
|
+
method: 'PATCH',
|
|
245
|
+
path: `/api/todos/${encodeURIComponent(todoId)}/progress-description`,
|
|
246
|
+
body: { progress_description: options.message },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
print(await resources.updateTodoProgress(todoId, options.message));
|
|
252
|
+
}));
|
|
253
|
+
const knowledge = program.command('knowledge');
|
|
254
|
+
knowledge
|
|
255
|
+
.command('list')
|
|
256
|
+
.option('--project <id>')
|
|
257
|
+
.option('--q <keyword>')
|
|
258
|
+
.option('--kind <kind>')
|
|
259
|
+
.option('--status <status>')
|
|
260
|
+
.option('--space <space>', 'personal|team')
|
|
261
|
+
.option('--person <name-or-id>')
|
|
262
|
+
.option('--page <number>', 'page number', Number)
|
|
263
|
+
.option('--page-size <number>', 'items per page', Number)
|
|
264
|
+
.option('--json')
|
|
265
|
+
.action(action(async (options) => print(await resources.listKnowledge(options), options.json)));
|
|
266
|
+
knowledge
|
|
267
|
+
.command('show <knowledgeId>')
|
|
268
|
+
.option('--json')
|
|
269
|
+
.action(action(async (knowledgeId, options) => print(await resources.showKnowledge(knowledgeId), options.json)));
|
|
270
|
+
knowledge
|
|
271
|
+
.command('search <keyword>')
|
|
272
|
+
.option('--project <id>')
|
|
273
|
+
.option('--person <name-or-id>')
|
|
274
|
+
.option('--scope <scope>', 'team|managed|participating|assigned', 'team')
|
|
275
|
+
.option('--limit <number>', 'maximum results', Number)
|
|
276
|
+
.option('--json')
|
|
277
|
+
.action(action(async (keyword, options) => print(await resources.searchKnowledge(keyword, options), options.json)));
|
|
278
|
+
knowledge
|
|
279
|
+
.command('sources')
|
|
280
|
+
.option('--path <path>')
|
|
281
|
+
.option('--q <keyword>')
|
|
282
|
+
.option('--json')
|
|
283
|
+
.action(action(async (options) => print(await resources.listKnowledgeSources({ path: options.path, q: options.q }), options.json)));
|
|
284
|
+
knowledge
|
|
285
|
+
.command('source <path>')
|
|
286
|
+
.option('--json')
|
|
287
|
+
.action(action(async (filePath, options) => print(await resources.readKnowledgeSource(filePath), options.json)));
|
|
288
|
+
const worklogs = program.command('worklogs');
|
|
289
|
+
worklogs
|
|
290
|
+
.command('list')
|
|
291
|
+
.option('--today')
|
|
292
|
+
.option('--window <window>')
|
|
293
|
+
.option('--json')
|
|
294
|
+
.action(action(async (options) => print(await resources.listWorklogs(options), options.json)));
|
|
295
|
+
worklogs
|
|
296
|
+
.command('record')
|
|
297
|
+
.option('--summary <text>')
|
|
298
|
+
.option('--type <type>', 'progress|blocker|decision|commit|note|meeting', 'note')
|
|
299
|
+
.option('--dry-run')
|
|
300
|
+
.action(action(async (options) => {
|
|
301
|
+
requireOption(options.summary, '--summary');
|
|
302
|
+
if (options.dryRun) {
|
|
303
|
+
print({
|
|
304
|
+
dry_run: true,
|
|
305
|
+
request: {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
path: '/api/work-logs',
|
|
308
|
+
body: {
|
|
309
|
+
summary: options.summary,
|
|
310
|
+
event_type: options.type,
|
|
311
|
+
source: 'manual',
|
|
312
|
+
metadata: { source_client: 'ot-cli' },
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
print(await resources.recordWorklog(options.summary, options.type));
|
|
319
|
+
}));
|
|
320
|
+
registerCollaborationCommands(program, { goals, todos, knowledge, worklogs }, { print, fatal });
|
|
321
|
+
program
|
|
322
|
+
.command('report [subcommand]')
|
|
323
|
+
.description('daily report commands')
|
|
324
|
+
.allowUnknownOption()
|
|
325
|
+
.action(action((subcommand) => {
|
|
326
|
+
if (subcommand === 'draft') {
|
|
327
|
+
const options = parseSimpleOptions(process.argv.slice(4));
|
|
328
|
+
print({ path: draftDailyReport(findWorkspaceRoot(), options) });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
throw new Error('Usage: ot report draft [--today] [--output <path>]');
|
|
332
|
+
}));
|
|
333
|
+
program
|
|
334
|
+
.command('mcp [subcommand]')
|
|
335
|
+
.description('MCP server commands')
|
|
336
|
+
.allowUnknownOption()
|
|
337
|
+
.action(action(async (subcommand) => {
|
|
338
|
+
if (subcommand === 'serve') {
|
|
339
|
+
await serveMcp();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
throw new Error('Usage: ot mcp serve');
|
|
343
|
+
}));
|
|
344
|
+
if (!runSpecialNestedCommand(process.argv)) {
|
|
345
|
+
try {
|
|
346
|
+
program.parse(process.argv);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
if (!isExpectedCommanderExit(err))
|
|
350
|
+
fatal(err);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function action(fn) {
|
|
354
|
+
return (...args) => {
|
|
355
|
+
Promise.resolve(fn(...args)).catch(fatal);
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function fatal(err) {
|
|
359
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
360
|
+
if (process.argv.includes('--json') || process.argv.includes('-j')) {
|
|
361
|
+
const error = err instanceof ApiError
|
|
362
|
+
? {
|
|
363
|
+
code: err.code,
|
|
364
|
+
message,
|
|
365
|
+
method: err.method,
|
|
366
|
+
path: err.path,
|
|
367
|
+
status: err.status,
|
|
368
|
+
detail: err.detail,
|
|
369
|
+
}
|
|
370
|
+
: { code: 'cli_error', message };
|
|
371
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error }, null, 2)}\n`);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
process.stderr.write(`ot: ${message}\n`);
|
|
375
|
+
}
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
function installTarget(target, workspace, scope, withAgentHooks) {
|
|
379
|
+
if (target === 'git')
|
|
380
|
+
installGitHook(workspace);
|
|
381
|
+
if (target === 'claude')
|
|
382
|
+
installClaude({ workspace, scope, withAgentHooks });
|
|
383
|
+
if (target === 'codex')
|
|
384
|
+
installCodex({ workspace, scope, withAgentHooks });
|
|
385
|
+
if (target === 'cursor')
|
|
386
|
+
installCursor({ workspace, scope, withAgentHooks });
|
|
387
|
+
if (target === 'qoder')
|
|
388
|
+
installQoder({ workspace, scope, withAgentHooks });
|
|
389
|
+
if (target === 'kiro')
|
|
390
|
+
installKiro({ workspace, scope, withAgentHooks });
|
|
391
|
+
}
|
|
392
|
+
function uninstallTarget(target, workspace, scope) {
|
|
393
|
+
const options = { workspace, scope, withAgentHooks: true };
|
|
394
|
+
if (target === 'git')
|
|
395
|
+
uninstallGitHook(workspace);
|
|
396
|
+
if (target === 'claude')
|
|
397
|
+
uninstallClaude(options);
|
|
398
|
+
if (target === 'codex')
|
|
399
|
+
uninstallCodex(options);
|
|
400
|
+
if (target === 'cursor')
|
|
401
|
+
uninstallCursor(options);
|
|
402
|
+
if (target === 'qoder')
|
|
403
|
+
uninstallQoder(options);
|
|
404
|
+
if (target === 'kiro')
|
|
405
|
+
uninstallKiro(options);
|
|
406
|
+
}
|
|
407
|
+
function requireOption(value, name) {
|
|
408
|
+
if (value == null || value === '') {
|
|
409
|
+
throw new Error(`Missing required option ${name}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function expandTargets(target) {
|
|
413
|
+
if (target === 'all')
|
|
414
|
+
return ['git', 'claude', 'codex', 'cursor', 'qoder', 'kiro'];
|
|
415
|
+
return [target];
|
|
416
|
+
}
|
|
417
|
+
function print(value, asJson = true) {
|
|
418
|
+
if (asJson) {
|
|
419
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
423
|
+
}
|
|
424
|
+
async function readStdin() {
|
|
425
|
+
const chunks = [];
|
|
426
|
+
for await (const chunk of process.stdin)
|
|
427
|
+
chunks.push(Buffer.from(chunk));
|
|
428
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
429
|
+
}
|
|
430
|
+
function updateGitignore(workspace) {
|
|
431
|
+
const gitignorePath = `${workspace}/.gitignore`;
|
|
432
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
433
|
+
if (existing.split(/\r?\n/).includes('.openturtle-cli/'))
|
|
434
|
+
return;
|
|
435
|
+
fs.writeFileSync(gitignorePath, `${existing.trimEnd()}${existing.trim() ? '\n' : ''}.openturtle-cli/\n`, 'utf-8');
|
|
436
|
+
}
|
|
437
|
+
function runSpecialNestedCommand(argv) {
|
|
438
|
+
const [, , first, second, ...rest] = argv;
|
|
439
|
+
if (first === 'mcp' && second === 'serve') {
|
|
440
|
+
void serveMcp().catch(fatal);
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (first === 'report' && second === 'draft') {
|
|
444
|
+
const options = parseSimpleOptions(rest);
|
|
445
|
+
print({ path: draftDailyReport(findWorkspaceRoot(), options) });
|
|
446
|
+
process.exit(0);
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
function parseSimpleOptions(args) {
|
|
452
|
+
const options = {};
|
|
453
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
454
|
+
const arg = args[index];
|
|
455
|
+
if (arg === '--today')
|
|
456
|
+
options.today = true;
|
|
457
|
+
if (arg === '--output') {
|
|
458
|
+
options.output = args[index + 1];
|
|
459
|
+
index += 1;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return options;
|
|
463
|
+
}
|
|
464
|
+
function isExpectedCommanderExit(err) {
|
|
465
|
+
return Boolean(err &&
|
|
466
|
+
typeof err === 'object' &&
|
|
467
|
+
'code' in err &&
|
|
468
|
+
String(err.code).startsWith('commander.'));
|
|
469
|
+
}
|