@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.
@@ -0,0 +1,315 @@
1
+ import { ApiClient, listQuery } from '../core/api-client.js';
2
+ import { collectOption, parseKeyValueOptions, readJsonBody } from '../core/input.js';
3
+ export function createCollaborationCommands(client = new ApiClient()) {
4
+ const mutate = async (method, path, body, dryRun = false) => {
5
+ if (dryRun)
6
+ return dryRunRequest(method, path, body);
7
+ return client.request(method, path, body);
8
+ };
9
+ return {
10
+ listProjects: (options) => client.get('/api/projects', listQuery(options)),
11
+ showProject: async (projectId, includeMembers = false) => {
12
+ const project = await client.get(`/api/projects/${encodeURIComponent(projectId)}`);
13
+ if (!includeMembers)
14
+ return project;
15
+ const members = await client.get(`/api/projects/${encodeURIComponent(projectId)}/members`);
16
+ return { project, members };
17
+ },
18
+ async resolveProject(query) {
19
+ const response = await client.get('/api/projects', {
20
+ keyword: query,
21
+ activated_only: false,
22
+ page_size: 100,
23
+ });
24
+ const items = Array.isArray(response) ? response : response.items || [];
25
+ const exact = items.find((item) => item.id === query || String(item.name || '').toLowerCase() === query.toLowerCase());
26
+ return {
27
+ resolved: Boolean(exact),
28
+ project: exact || null,
29
+ candidates: exact ? [] : items,
30
+ };
31
+ },
32
+ async listRoster(teamId) {
33
+ const team = teamId ? { id: teamId } : await client.get('/api/teams/me');
34
+ const resolvedTeamId = String(team.id || teamId || '');
35
+ if (!resolvedTeamId)
36
+ throw new Error('Current team response did not include an id');
37
+ const [members, profile, rosterProfile] = await Promise.all([
38
+ client.get(`/api/teams/${encodeURIComponent(resolvedTeamId)}/members`, { page_size: 100 }),
39
+ client.get('/api/me/profile').catch(() => null),
40
+ client.get('/api/me/roster-profile').catch(() => null),
41
+ ]);
42
+ return { team, profile, roster_profile: rosterProfile, members };
43
+ },
44
+ resolveContext: (body) => client.post('/api/project-context/resolve', body),
45
+ getContextItem: (sourceType, sourceId, options) => client.get(`/api/project-context/items/${encodeURIComponent(sourceType)}/${encodeURIComponent(sourceId)}`, listQuery(options)),
46
+ relations: (knowledgeId) => client.get(`/api/memory/objects/${encodeURIComponent(knowledgeId)}/relations`),
47
+ saveKnowledge: (knowledgeId, body, dryRun = false) => mutate(knowledgeId ? 'PATCH' : 'POST', knowledgeId ? `/api/memory/objects/${encodeURIComponent(knowledgeId)}` : '/api/memory/objects', body, dryRun),
48
+ attachKnowledge: async (knowledgeId, files, dryRun = false) => {
49
+ const path = `/api/memory/objects/${encodeURIComponent(knowledgeId)}/attachments`;
50
+ if (dryRun)
51
+ return dryRunRequest('POST', path, { files });
52
+ return client.upload('POST', path, files.map((filePath) => ({ field: 'files', filePath })));
53
+ },
54
+ readKnowledgeAttachment: (knowledgeId, attachmentId, truncate) => client.get(`/api/memory/objects/${encodeURIComponent(knowledgeId)}/attachments/${encodeURIComponent(attachmentId)}/content`, { truncate }),
55
+ downloadKnowledgeAttachment: (knowledgeId, attachmentId, output, force = false, dryRun = false) => {
56
+ const path = `/api/memory/objects/${encodeURIComponent(knowledgeId)}/attachments/${encodeURIComponent(attachmentId)}/download`;
57
+ if (dryRun)
58
+ return Promise.resolve(dryRunDownload(path, output, force));
59
+ return client.download(path, output, force);
60
+ },
61
+ listMeetings: (options) => client.get('/api/meeting', listQuery(options)),
62
+ showMeeting: (meetingId) => client.get(`/api/meeting/${encodeURIComponent(meetingId)}/detail`),
63
+ meetingPart: (meetingId, part) => client.get(`/api/meeting/${encodeURIComponent(meetingId)}/${part}`),
64
+ createMeeting: (body, dryRun = false) => mutate('POST', '/api/meeting', body, dryRun),
65
+ uploadMeeting: async (filePath, options, dryRun = false) => {
66
+ const fields = listQuery({ project_id: options.project, goal_id: options.goal, title: options.title });
67
+ if (dryRun)
68
+ return dryRunRequest('POST', '/api/meeting/upload', { file: filePath, ...fields });
69
+ return client.upload('POST', '/api/meeting/upload', [{ field: 'file', filePath }], fields);
70
+ },
71
+ retryMeeting: (meetingId, dryRun = false) => mutate('POST', `/api/meeting/${encodeURIComponent(meetingId)}/processing/retry`, undefined, dryRun),
72
+ publishMeetingKnowledge: (meetingId, dryRun = false) => mutate('POST', `/api/meeting/${encodeURIComponent(meetingId)}/knowledge/publish`, undefined, dryRun),
73
+ downloadMeetingRecording: (meetingId, output, force = false, dryRun = false) => {
74
+ const path = `/api/meeting/${encodeURIComponent(meetingId)}/recording/download`;
75
+ if (dryRun)
76
+ return Promise.resolve(dryRunDownload(path, output, force));
77
+ return client.download(path, output, force);
78
+ },
79
+ listProjectUpdates: (meetingId) => client.get(`/api/meeting/${encodeURIComponent(meetingId)}/project-updates`),
80
+ editProjectUpdate: (meetingId, updateId, body, dryRun = false) => mutate('PATCH', `/api/meeting/${encodeURIComponent(meetingId)}/project-updates/${encodeURIComponent(updateId)}`, body, dryRun),
81
+ confirmProjectUpdates: (meetingId, updateIds, projectId, knowledgeId, dryRun = false) => mutate('POST', `/api/meeting/${encodeURIComponent(meetingId)}/project-updates/confirm`, { project_id: projectId, knowledge_id: knowledgeId, update_ids: updateIds }, dryRun),
82
+ ignoreProjectUpdate: (meetingId, updateId, dryRun = false) => mutate('POST', `/api/meeting/${encodeURIComponent(meetingId)}/project-updates/${encodeURIComponent(updateId)}/ignore`, undefined, dryRun),
83
+ createGoal: (body, dryRun = false) => mutate('POST', '/api/goals', body, dryRun),
84
+ updateGoal: (goalId, body, dryRun = false) => mutate('PATCH', `/api/goals/${encodeURIComponent(goalId)}`, body, dryRun),
85
+ submitGoalReview: (goalId, body, dryRun = false) => mutate('POST', `/api/goals/${encodeURIComponent(goalId)}/submit-review`, body, dryRun),
86
+ reviewGoal: (goalId, body, dryRun = false) => mutate('POST', `/api/goals/${encodeURIComponent(goalId)}/review`, body, dryRun),
87
+ createTodo: (body, dryRun = false) => mutate('POST', '/api/todos', body, dryRun),
88
+ updateTodo: (todoId, body, dryRun = false) => mutate('PATCH', `/api/todos/${encodeURIComponent(todoId)}`, body, dryRun),
89
+ submitTodoReview: (todoId, body, dryRun = false) => mutate('POST', `/api/todos/${encodeURIComponent(todoId)}/submit-review`, body, dryRun),
90
+ reviewTodo: (todoId, body, dryRun = false) => mutate('POST', `/api/todos/${encodeURIComponent(todoId)}/review`, body, dryRun),
91
+ claimTodo: (todoId, dryRun = false) => mutate('POST', `/api/todos/${encodeURIComponent(todoId)}/claim`, undefined, dryRun),
92
+ assignTodo: (todoId, assigneeId, dryRun = false) => mutate('POST', `/api/todos/${encodeURIComponent(todoId)}/assign`, { assignee_id: assigneeId }, dryRun),
93
+ createWorklog: (body, dryRun = false) => mutate('POST', '/api/work-logs', body, dryRun),
94
+ requestGet: (path, query) => client.get(path, query),
95
+ };
96
+ }
97
+ export function registerCollaborationCommands(program, groups, callbacks) {
98
+ const commands = createCollaborationCommands();
99
+ const action = (fn) => (...args) => {
100
+ Promise.resolve(fn(...args)).catch(callbacks.fatal);
101
+ };
102
+ const print = callbacks.print;
103
+ const projects = program.command('projects').description('Project discovery and context binding');
104
+ projects
105
+ .command('list')
106
+ .option('--keyword <text>')
107
+ .option('--page <number>', 'page number', Number)
108
+ .option('--page-size <number>', 'items per page', Number)
109
+ .option('--all', 'include inactive projects')
110
+ .option('--todo-stats')
111
+ .action(action(async (options) => print(await commands.listProjects({
112
+ keyword: options.keyword,
113
+ page: options.page,
114
+ page_size: options.pageSize,
115
+ activated_only: options.all ? false : undefined,
116
+ include_todo_stats: options.todoStats,
117
+ }))));
118
+ projects
119
+ .command('show <projectId>')
120
+ .option('--members')
121
+ .action(action(async (projectId, options) => print(await commands.showProject(projectId, Boolean(options.members)))));
122
+ projects
123
+ .command('resolve <query>')
124
+ .action(action(async (query) => print(await commands.resolveProject(query))));
125
+ program
126
+ .command('roster')
127
+ .description('Current team roster')
128
+ .command('list')
129
+ .option('--team <id>')
130
+ .action(action(async (options) => print(await commands.listRoster(options.team))));
131
+ const context = program.command('context').description('Viewer-scoped project context');
132
+ context
133
+ .command('get')
134
+ .option('--project <id>')
135
+ .option('--goal <id>')
136
+ .option('--person <id>')
137
+ .option('--query <text>', 'repeatable search term', collectOption, [])
138
+ .option('--section <name>', 'repeatable section filter', collectOption, [])
139
+ .option('--match <mode>', 'any|all|exact', 'any')
140
+ .option('--mode <mode>', 'brief|search', 'brief')
141
+ .option('--limit <number>', 'maximum context items', Number)
142
+ .action(action(async (options) => print(await commands.resolveContext({
143
+ project_id: options.project,
144
+ goal_id: options.goal,
145
+ person_id: options.person,
146
+ queries: options.query,
147
+ sections: options.section.length ? options.section : undefined,
148
+ match_mode: options.match,
149
+ mode: options.mode,
150
+ limit: options.limit,
151
+ }))));
152
+ context
153
+ .command('item <sourceType> <sourceId>')
154
+ .option('--project <id>')
155
+ .option('--goal <id>')
156
+ .option('--meeting <id>')
157
+ .option('--knowledge <id>')
158
+ .action(action(async (sourceType, sourceId, options) => print(await commands.getContextItem(sourceType, sourceId, {
159
+ project_id: options.project,
160
+ goal_id: options.goal,
161
+ meeting_id: options.meeting,
162
+ knowledge_id: options.knowledge,
163
+ }))));
164
+ groups.knowledge
165
+ .command('relations <knowledgeId>')
166
+ .action(action(async (knowledgeId) => print(await commands.relations(knowledgeId))));
167
+ groups.knowledge
168
+ .command('save [knowledgeId]')
169
+ .requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
170
+ .option('--dry-run')
171
+ .action(action(async (knowledgeId, options) => print(await commands.saveKnowledge(knowledgeId, await readJsonBody(options.bodyFile), Boolean(options.dryRun)))));
172
+ groups.knowledge
173
+ .command('attach <knowledgeId> <files...>')
174
+ .option('--dry-run')
175
+ .action(action(async (knowledgeId, files, options) => print(await commands.attachKnowledge(knowledgeId, files, Boolean(options.dryRun)))));
176
+ groups.knowledge
177
+ .command('attachment-content <knowledgeId> <attachmentId>')
178
+ .option('--truncate <characters>', 'maximum extracted text characters', Number)
179
+ .action(action(async (knowledgeId, attachmentId, options) => print(await commands.readKnowledgeAttachment(knowledgeId, attachmentId, options.truncate))));
180
+ groups.knowledge
181
+ .command('attachment-download <knowledgeId> <attachmentId>')
182
+ .option('--output <path>')
183
+ .option('--force')
184
+ .option('--dry-run')
185
+ .action(action(async (knowledgeId, attachmentId, options) => print(await commands.downloadKnowledgeAttachment(knowledgeId, attachmentId, options.output, Boolean(options.force), Boolean(options.dryRun)))));
186
+ const meetings = program.command('meetings').description('Meeting upload, minutes, knowledge, and project updates');
187
+ meetings
188
+ .command('list')
189
+ .option('--project <id>')
190
+ .option('--status <status>')
191
+ .option('--page <number>', 'page number', Number)
192
+ .option('--page-size <number>', 'items per page', Number)
193
+ .action(action(async (options) => print(await commands.listMeetings({
194
+ project_id: options.project,
195
+ status: options.status,
196
+ page: options.page,
197
+ page_size: options.pageSize,
198
+ }))));
199
+ meetings
200
+ .command('show <meetingId>')
201
+ .action(action(async (meetingId) => print(await commands.showMeeting(meetingId))));
202
+ for (const part of ['transcript', 'minutes', 'participants', 'processing']) {
203
+ meetings
204
+ .command(`${part} <meetingId>`)
205
+ .action(action(async (meetingId) => print(await commands.meetingPart(meetingId, part))));
206
+ }
207
+ meetings
208
+ .command('create')
209
+ .requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
210
+ .option('--dry-run')
211
+ .action(action(async (options) => print(await commands.createMeeting(await readJsonBody(options.bodyFile), Boolean(options.dryRun)))));
212
+ meetings
213
+ .command('upload <file>')
214
+ .requiredOption('--project <id>')
215
+ .option('--goal <id>')
216
+ .option('--title <text>')
217
+ .option('--dry-run')
218
+ .action(action(async (file, options) => print(await commands.uploadMeeting(file, options, Boolean(options.dryRun)))));
219
+ meetings
220
+ .command('retry <meetingId>')
221
+ .option('--dry-run')
222
+ .action(action(async (meetingId, options) => print(await commands.retryMeeting(meetingId, Boolean(options.dryRun)))));
223
+ meetings
224
+ .command('publish <meetingId>')
225
+ .option('--dry-run')
226
+ .action(action(async (meetingId, options) => print(await commands.publishMeetingKnowledge(meetingId, Boolean(options.dryRun)))));
227
+ meetings
228
+ .command('recording <meetingId>')
229
+ .option('--output <path>')
230
+ .option('--force')
231
+ .option('--dry-run')
232
+ .action(action(async (meetingId, options) => print(await commands.downloadMeetingRecording(meetingId, options.output, Boolean(options.force), Boolean(options.dryRun)))));
233
+ const updates = meetings.command('updates').description('Review meeting-derived project updates');
234
+ updates
235
+ .command('list <meetingId>')
236
+ .action(action(async (meetingId) => print(await commands.listProjectUpdates(meetingId))));
237
+ updates
238
+ .command('edit <meetingId> <updateId>')
239
+ .requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
240
+ .option('--dry-run')
241
+ .action(action(async (meetingId, updateId, options) => print(await commands.editProjectUpdate(meetingId, updateId, await readJsonBody(options.bodyFile), Boolean(options.dryRun)))));
242
+ updates
243
+ .command('confirm <meetingId> [updateIds...]')
244
+ .requiredOption('--knowledge <id>', 'published meeting knowledge id')
245
+ .option('--project <id>')
246
+ .option('--all', 'confirm every remaining draft update')
247
+ .option('--dry-run')
248
+ .action(action(async (meetingId, updateIds, options) => {
249
+ if (!updateIds.length && !options.all) {
250
+ throw new Error('Provide at least one update id, or pass --all after reviewing the draft list');
251
+ }
252
+ if (updateIds.length && options.all)
253
+ throw new Error('Use explicit update ids or --all, not both');
254
+ print(await commands.confirmProjectUpdates(meetingId, updateIds, options.project, options.knowledge, Boolean(options.dryRun)));
255
+ }));
256
+ updates
257
+ .command('ignore <meetingId> <updateId>')
258
+ .option('--dry-run')
259
+ .action(action(async (meetingId, updateId, options) => print(await commands.ignoreProjectUpdate(meetingId, updateId, Boolean(options.dryRun)))));
260
+ registerJsonMutation(groups.goals, 'create', [], (args, body, dryRun) => commands.createGoal(body, dryRun), action, print);
261
+ registerJsonMutation(groups.goals, 'update <goalId>', ['goalId'], (args, body, dryRun) => commands.updateGoal(args.goalId, body, dryRun), action, print);
262
+ registerJsonMutation(groups.goals, 'submit-review <goalId>', ['goalId'], (args, body, dryRun) => commands.submitGoalReview(args.goalId, body, dryRun), action, print);
263
+ registerJsonMutation(groups.goals, 'review <goalId>', ['goalId'], (args, body, dryRun) => commands.reviewGoal(args.goalId, body, dryRun), action, print);
264
+ registerJsonMutation(groups.todos, 'create', [], (args, body, dryRun) => commands.createTodo(body, dryRun), action, print);
265
+ registerJsonMutation(groups.todos, 'update <todoId>', ['todoId'], (args, body, dryRun) => commands.updateTodo(args.todoId, body, dryRun), action, print);
266
+ registerJsonMutation(groups.todos, 'submit-review <todoId>', ['todoId'], (args, body, dryRun) => commands.submitTodoReview(args.todoId, body, dryRun), action, print);
267
+ registerJsonMutation(groups.todos, 'review <todoId>', ['todoId'], (args, body, dryRun) => commands.reviewTodo(args.todoId, body, dryRun), action, print);
268
+ groups.todos
269
+ .command('claim <todoId>')
270
+ .option('--dry-run')
271
+ .action(action(async (todoId, options) => print(await commands.claimTodo(todoId, Boolean(options.dryRun)))));
272
+ groups.todos
273
+ .command('assign <todoId> <assigneeId>')
274
+ .option('--dry-run')
275
+ .action(action(async (todoId, assigneeId, options) => print(await commands.assignTodo(todoId, assigneeId, Boolean(options.dryRun)))));
276
+ registerJsonMutation(groups.worklogs, 'create', [], (args, body, dryRun) => commands.createWorklog(body, dryRun), action, print);
277
+ program
278
+ .command('request')
279
+ .description('Read-only raw API escape hatch')
280
+ .command('get <path>')
281
+ .option('--query <key=value>', 'repeatable query parameter', collectOption, [])
282
+ .action(action(async (path, options) => {
283
+ if (!path.startsWith('/api/'))
284
+ throw new Error('Raw request paths must start with /api/');
285
+ print(await commands.requestGet(path, parseKeyValueOptions(options.query)));
286
+ }));
287
+ }
288
+ function registerJsonMutation(parent, name, argumentNames, execute, action, print) {
289
+ const command = parent
290
+ .command(name)
291
+ .requiredOption('--body-file <path>', 'JSON body file, or - for stdin')
292
+ .option('--dry-run');
293
+ command.action(action(async (...values) => {
294
+ const options = values[argumentNames.length];
295
+ const args = Object.fromEntries(argumentNames.map((argument, index) => [argument, String(values[index])]));
296
+ print(await execute(args, await readJsonBody(options.bodyFile), Boolean(options.dryRun)));
297
+ }));
298
+ }
299
+ function dryRunRequest(method, path, body) {
300
+ return {
301
+ dry_run: true,
302
+ request: {
303
+ method,
304
+ path,
305
+ ...(body === undefined ? {} : { body }),
306
+ },
307
+ };
308
+ }
309
+ function dryRunDownload(path, output, force) {
310
+ return {
311
+ dry_run: true,
312
+ request: { method: 'GET', path },
313
+ output: { path: output || null, force },
314
+ };
315
+ }
@@ -0,0 +1,162 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { compactJson } from '../core/json.js';
4
+ import { findWorkspaceRoot } from '../core/paths.js';
5
+ import { initProjectStore, recordCommit, recordHookRun, upsertEvent } from '../core/store.js';
6
+ const EVENT_MAP = {
7
+ UserPromptSubmit: 'agent.user_prompt',
8
+ userPromptSubmit: 'agent.user_prompt',
9
+ user_prompt_submit: 'agent.user_prompt',
10
+ pre_tool_use: 'agent.pre_tool_use',
11
+ PreToolUse: 'agent.pre_tool_use',
12
+ preToolUse: 'agent.pre_tool_use',
13
+ post_tool_use: 'agent.post_tool_use',
14
+ PostToolUse: 'agent.post_tool_use',
15
+ postToolUse: 'agent.post_tool_use',
16
+ PostToolUseFailure: 'agent.post_tool_use_failure',
17
+ postToolUseFailure: 'agent.post_tool_use_failure',
18
+ Stop: 'agent.stop',
19
+ stop: 'agent.stop',
20
+ agentSpawn: 'agent.spawn',
21
+ AgentSpawn: 'agent.spawn',
22
+ };
23
+ export function ingestHookPayload(store, input) {
24
+ const type = EVENT_MAP[input.event] || EVENT_MAP[camelToPascal(input.event)] || 'agent.session_imported';
25
+ const sourceId = stringValue(input.payload.session_id ||
26
+ input.payload.sessionId ||
27
+ input.payload.conversation_id ||
28
+ input.payload.conversationId ||
29
+ input.payload.request_id ||
30
+ input.payload.requestId);
31
+ const sequence = stringValue(input.payload.sequence ||
32
+ input.payload.sequence_number ||
33
+ input.payload.event_id ||
34
+ input.payload.eventId ||
35
+ input.payload.tool_use_id ||
36
+ input.payload.toolUseId ||
37
+ input.payload.request_id ||
38
+ input.payload.requestId);
39
+ const workspacePath = stringValue(input.payload.cwd || input.payload.workspace_path || input.payload.workspacePath) || store.workspace;
40
+ const summary = summarizeHookPayload(type, input.payload);
41
+ const stableId = sourceId || hashText(compactJson(input.payload)).slice(0, 16);
42
+ const stableSequence = sequence || hashText(`${input.event}:${summary}:${compactJson(input.payload)}`).slice(0, 16);
43
+ const dedupeKey = `${input.target}:${type}:${stableId}:${stableSequence}`;
44
+ const result = upsertEvent(store, {
45
+ type,
46
+ source: input.target,
47
+ sourceId,
48
+ workspacePath,
49
+ summary,
50
+ rawJson: input.payload,
51
+ occurredAt: stringValue(input.payload.timestamp || input.payload.occurred_at) || new Date().toISOString(),
52
+ dedupeKey,
53
+ });
54
+ return { ok: true, inserted: result.inserted, dedupeKey };
55
+ }
56
+ export function ingestRawHook(options) {
57
+ const started = Date.now();
58
+ const workspace = options.workspace || findWorkspaceRoot();
59
+ const store = initProjectStore(workspace);
60
+ try {
61
+ const payload = options.stdin.trim() ? JSON.parse(options.stdin) : {};
62
+ const normalized = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : { value: payload };
63
+ const result = ingestHookPayload(store, {
64
+ target: options.target,
65
+ event: options.event,
66
+ payload: normalized,
67
+ });
68
+ recordHookRun(store, {
69
+ target: options.target,
70
+ eventType: options.event,
71
+ exitCode: 0,
72
+ durationMs: Date.now() - started,
73
+ rawInputJson: options.stdin,
74
+ });
75
+ return result;
76
+ }
77
+ catch (err) {
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ recordHookRun(store, {
80
+ target: options.target,
81
+ eventType: options.event,
82
+ exitCode: 0,
83
+ durationMs: Date.now() - started,
84
+ rawInputJson: options.stdin,
85
+ error: message,
86
+ });
87
+ return { ok: false, error: message };
88
+ }
89
+ }
90
+ export function recordGitPostCommit(workspace = findWorkspaceRoot()) {
91
+ const store = initProjectStore(workspace);
92
+ const hash = git(workspace, ['rev-parse', 'HEAD']).trim();
93
+ const message = git(workspace, ['log', '-1', '--pretty=%B']).trim();
94
+ const author = git(workspace, ['log', '-1', '--pretty=%an <%ae>']).trim();
95
+ const diffStat = git(workspace, ['diff-tree', '--stat', '--no-commit-id', '--root', '-r', hash]).trim();
96
+ const changedFiles = git(workspace, ['diff-tree', '--no-commit-id', '--name-only', '--root', '-r', hash])
97
+ .split(/\r?\n/)
98
+ .map((item) => item.trim())
99
+ .filter(Boolean);
100
+ const occurredAt = git(workspace, ['log', '-1', '--pretty=%cI']).trim() || new Date().toISOString();
101
+ recordCommit(store, { hash, message, author, diffStat, changedFiles, occurredAt });
102
+ upsertEvent(store, {
103
+ type: 'git.post_commit',
104
+ source: 'git',
105
+ sourceId: hash,
106
+ workspacePath: workspace,
107
+ summary: message.split(/\r?\n/)[0] || hash,
108
+ rawJson: { hash, message, author, diffStat, changedFiles },
109
+ occurredAt,
110
+ dedupeKey: `git:git.post_commit:${hash}:${hash}`,
111
+ });
112
+ }
113
+ function summarizeHookPayload(type, payload) {
114
+ if (type === 'agent.user_prompt') {
115
+ return truncate(stringValue(payload.prompt || payload.message || payload.text || payload.input) || 'User prompt');
116
+ }
117
+ const toolName = stringValue(payload.tool_name || payload.toolName || payload.name || payload.tool);
118
+ if (toolName)
119
+ return `${eventLabel(type)} ${toolName}`;
120
+ return eventLabel(type);
121
+ }
122
+ function eventLabel(type) {
123
+ return type.replace(/^agent\./, '').replaceAll('_', ' ');
124
+ }
125
+ function stringValue(value) {
126
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
127
+ }
128
+ function truncate(value, limit = 240) {
129
+ return value.length <= limit ? value : `${value.slice(0, limit - 1)}...`;
130
+ }
131
+ function camelToPascal(value) {
132
+ return value ? `${value[0]?.toUpperCase()}${value.slice(1)}` : value;
133
+ }
134
+ function hashText(value) {
135
+ let hash = 0x811c9dc5;
136
+ for (let index = 0; index < value.length; index += 1) {
137
+ hash ^= value.charCodeAt(index);
138
+ hash = Math.imul(hash, 0x01000193);
139
+ }
140
+ return (hash >>> 0).toString(16);
141
+ }
142
+ function git(cwd, args) {
143
+ return execFileSync('git', args, {
144
+ cwd,
145
+ encoding: 'utf-8',
146
+ stdio: ['ignore', 'pipe', 'ignore'],
147
+ });
148
+ }
149
+ export function hookCommandArgs(target, event) {
150
+ return ['hook', 'ingest', '--target', target, '--event', event, '--stdin-json'];
151
+ }
152
+ export function hookCommand(target, event) {
153
+ return ['ot', ...hookCommandArgs(target, event).map(shellQuote)].join(' ');
154
+ }
155
+ export function shellQuote(value) {
156
+ if (/^[A-Za-z0-9_./:=@-]+$/.test(value))
157
+ return value;
158
+ return `'${value.replaceAll("'", "'\\''")}'`;
159
+ }
160
+ export function workspaceFromPayload(payload, fallback = process.cwd()) {
161
+ return stringValue(payload.cwd || payload.workspacePath || payload.workspace_path) || path.resolve(fallback);
162
+ }
@@ -0,0 +1,118 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ApiClient, listQuery } from '../core/api-client.js';
4
+ import { readAuth } from '../core/auth.js';
5
+ import { initProjectStore, listEvents } from '../core/store.js';
6
+ export function createResourceCommands(client = new ApiClient()) {
7
+ return {
8
+ async listGoals(options) {
9
+ return client.get('/api/goals', listQuery(options));
10
+ },
11
+ async showGoal(goalId) {
12
+ return client.get(`/api/goals/${encodeURIComponent(goalId)}`);
13
+ },
14
+ async listTodos(options) {
15
+ return client.get('/api/todos', listQuery(options));
16
+ },
17
+ async showTodo(todoId) {
18
+ return client.get(`/api/todos/${encodeURIComponent(todoId)}`);
19
+ },
20
+ async updateTodoStatus(todoId, status) {
21
+ return client.patch(`/api/todos/${encodeURIComponent(todoId)}/status`, { status });
22
+ },
23
+ async updateTodoProgress(todoId, message) {
24
+ return client.patch(`/api/todos/${encodeURIComponent(todoId)}/progress-description`, {
25
+ progress_description: message,
26
+ });
27
+ },
28
+ async listKnowledge(options) {
29
+ return client.get('/api/memory/objects', knowledgeQuery(options));
30
+ },
31
+ async showKnowledge(knowledgeId) {
32
+ return client.get(`/api/memory/objects/${encodeURIComponent(knowledgeId)}`);
33
+ },
34
+ async searchKnowledge(keyword, options = {}) {
35
+ return client.get('/api/memory/search', listQuery({
36
+ q: keyword,
37
+ project_id: options.project,
38
+ person: options.person,
39
+ scope: options.scope,
40
+ limit: options.limit,
41
+ }));
42
+ },
43
+ async listKnowledgeSources(options) {
44
+ return client.get('/api/gitea/knowledge', listQuery({ path: options.path, q: options.q }));
45
+ },
46
+ async readKnowledgeSource(filePath) {
47
+ return client.get(`/api/gitea/knowledge/${encodeURIComponent(filePath)}`);
48
+ },
49
+ async listWorklogs(options) {
50
+ return client.get('/api/work-logs', listQuery(options));
51
+ },
52
+ async recordWorklog(summary, type = 'note') {
53
+ return client.post('/api/work-logs', {
54
+ summary,
55
+ event_type: type,
56
+ source: 'manual',
57
+ metadata: { source_client: 'ot-cli' },
58
+ });
59
+ },
60
+ };
61
+ }
62
+ function knowledgeQuery(options) {
63
+ return listQuery({
64
+ type: 'knowledge',
65
+ project_id: options.project_id || options.project,
66
+ q: options.q,
67
+ kind: options.kind,
68
+ status: options.status,
69
+ space: options.space,
70
+ person: options.person,
71
+ page: options.page,
72
+ page_size: options.pageSize,
73
+ });
74
+ }
75
+ export function draftDailyReport(workspace, options = {}) {
76
+ const store = initProjectStore(workspace);
77
+ const events = listEvents(store, 500);
78
+ const date = new Date().toISOString().slice(0, 10);
79
+ const lines = [
80
+ `# ${date} 工作日报草稿`,
81
+ '',
82
+ '## 今日记录',
83
+ '',
84
+ ...events.map((event) => `- ${event.occurred_at} [${event.source}/${event.type}] ${event.summary || ''}`),
85
+ '',
86
+ '## 提交与工具调用摘要',
87
+ '',
88
+ '请结合上述记录补充业务语义、风险和明日计划。',
89
+ '',
90
+ ];
91
+ const content = lines.join('\n');
92
+ const outputPath = options.output || path.join(store.dir, 'drafts', `${date}.md`);
93
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
94
+ fs.writeFileSync(outputPath, content, 'utf-8');
95
+ return outputPath;
96
+ }
97
+ export function doctor(workspace, target) {
98
+ const auth = readAuth();
99
+ const store = initProjectStore(workspace);
100
+ const projectDirExists = fs.existsSync(store.dir);
101
+ return {
102
+ cli: {
103
+ project_dir: store.dir,
104
+ project_dir_exists: projectDirExists,
105
+ global_dir: path.join(process.env.HOME || '', '.openturtle', 'cli'),
106
+ mcp_server_name: 'ot-cli',
107
+ },
108
+ auth: {
109
+ server: auth.server || null,
110
+ token_configured: Boolean(auth.token),
111
+ },
112
+ coexistence: {
113
+ avoids_project_dot_openturtle: true,
114
+ desktop_mcp_names_reserved: ['openturtle-knowledge', 'openturtle-automations', 'openturtle-todo'],
115
+ },
116
+ target: target || 'all',
117
+ };
118
+ }