cairn-work 1.0.2 → 1.2.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 +22 -0
- package/bin/cairn.js +11 -0
- package/lib/commands/comment.js +186 -0
- package/package.json +1 -1
- package/templates/workspace/TOOLS.md.template +1 -1
package/README.md
CHANGED
|
@@ -314,6 +314,28 @@ cairn log implement-auth "Implemented OAuth2 flow with GitHub provider"
|
|
|
314
314
|
cairn log implement-auth "Fixed edge case in token refresh" --title "Bug Fix"
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
+
### `cairn comment`
|
|
318
|
+
|
|
319
|
+
Add a comment that syncs to the Cairn app (visible in the web UI).
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Status update
|
|
323
|
+
cairn comment implement-auth "Started OAuth implementation"
|
|
324
|
+
|
|
325
|
+
# Ask a question
|
|
326
|
+
cairn comment implement-auth "Should we support Google SSO?" --type question
|
|
327
|
+
|
|
328
|
+
# Worker handoff
|
|
329
|
+
cairn comment implement-auth "Ready for code review" --type handoff --author "Engineer"
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Options:**
|
|
333
|
+
- `--type <type>`: Comment type (`progress`, `question`, `handoff`, `comment`)
|
|
334
|
+
- `--author <name>`: Author name (defaults to `$USER`)
|
|
335
|
+
- `--author-type <type>`: `worker` or `human` (defaults to `worker`)
|
|
336
|
+
|
|
337
|
+
Unlike `cairn note` (which writes to the task file), `cairn comment` writes to Convex and appears in the Cairn web app. Requires cairnsync authentication.
|
|
338
|
+
|
|
317
339
|
### `cairn update`
|
|
318
340
|
|
|
319
341
|
Update task properties programmatically.
|
package/bin/cairn.js
CHANGED
|
@@ -42,6 +42,7 @@ import search from '../lib/commands/search.js';
|
|
|
42
42
|
import triage from '../lib/commands/triage.js';
|
|
43
43
|
import learn from '../lib/commands/learn.js';
|
|
44
44
|
import worker from '../lib/commands/worker.js';
|
|
45
|
+
import comment from '../lib/commands/comment.js';
|
|
45
46
|
|
|
46
47
|
// Onboard command - workspace setup with context files
|
|
47
48
|
program
|
|
@@ -162,6 +163,16 @@ program
|
|
|
162
163
|
.option('--project <slug>', 'Project to search for the task')
|
|
163
164
|
.action(note);
|
|
164
165
|
|
|
166
|
+
// Comment command - add comment to Convex (shows in app)
|
|
167
|
+
program
|
|
168
|
+
.command('comment <task-slug> <message>')
|
|
169
|
+
.description('Add a comment to task (syncs to Cairn app)')
|
|
170
|
+
.option('--project <slug>', 'Project to search for the task')
|
|
171
|
+
.option('--type <type>', 'Comment type: question, progress, handoff, or comment', 'progress')
|
|
172
|
+
.option('--author <name>', 'Author name (defaults to $USER)')
|
|
173
|
+
.option('--author-type <type>', 'Author type: human or worker', 'worker')
|
|
174
|
+
.action(comment);
|
|
175
|
+
|
|
165
176
|
// View command - show full task details
|
|
166
177
|
program
|
|
167
178
|
.command('view <task-slug>')
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { resolveWorkspace } from '../setup/workspace.js';
|
|
6
|
+
import { findTaskBySlug, getAgentName } from '../utils/task-helpers.js';
|
|
7
|
+
|
|
8
|
+
const CONVEX_URL = 'https://admired-wildebeest-654.convex.cloud';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decode JWT payload without validation (for extracting userId)
|
|
12
|
+
*/
|
|
13
|
+
function decodeJwtPayload(token) {
|
|
14
|
+
try {
|
|
15
|
+
const parts = token.split('.');
|
|
16
|
+
if (parts.length !== 3) return null;
|
|
17
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
18
|
+
return JSON.parse(payload);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load auth token from workspace .cairn-token.json
|
|
26
|
+
*/
|
|
27
|
+
function loadAuthToken(workspacePath) {
|
|
28
|
+
const tokenPath = join(workspacePath, '.cairn-token.json');
|
|
29
|
+
|
|
30
|
+
if (!existsSync(tokenPath)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(tokenPath, 'utf-8');
|
|
36
|
+
const data = JSON.parse(raw);
|
|
37
|
+
|
|
38
|
+
if (!data.accessToken) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if token is expired
|
|
43
|
+
if (data.expiresAt && Date.now() >= data.expiresAt) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return data.accessToken;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build task path from project and slug
|
|
55
|
+
*/
|
|
56
|
+
function buildTaskPath(project, taskSlug) {
|
|
57
|
+
return `projects/${project}/tasks/${taskSlug}.md`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Call Convex mutation via HTTP
|
|
62
|
+
*/
|
|
63
|
+
async function callConvexMutation(functionName, args, token) {
|
|
64
|
+
const url = `${CONVEX_URL}/api/mutation`;
|
|
65
|
+
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Authorization': `Bearer ${token}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
path: functionName,
|
|
74
|
+
args,
|
|
75
|
+
format: 'json',
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
throw new Error(`Convex API error (${response.status}): ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await response.json();
|
|
85
|
+
|
|
86
|
+
if (result.status === 'error') {
|
|
87
|
+
throw new Error(result.errorMessage || 'Unknown Convex error');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result.value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default async function comment(taskSlug, message, options) {
|
|
94
|
+
// Try to resolve workspace, fallback to ~/pms for compatibility
|
|
95
|
+
let workspacePath = resolveWorkspace();
|
|
96
|
+
|
|
97
|
+
if (!workspacePath) {
|
|
98
|
+
// Fallback to ~/pms (common default)
|
|
99
|
+
const fallbackPath = join(homedir(), 'pms');
|
|
100
|
+
if (existsSync(join(fallbackPath, 'projects'))) {
|
|
101
|
+
workspacePath = fallbackPath;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!workspacePath) {
|
|
106
|
+
console.error(chalk.red('Error:'), 'No workspace found. Run:', chalk.cyan('cairn onboard'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!taskSlug) {
|
|
111
|
+
console.error(chalk.red('Error:'), 'Missing task slug');
|
|
112
|
+
console.log(chalk.dim('Usage:'), chalk.cyan('cairn comment <task-slug> <message>'));
|
|
113
|
+
console.log(chalk.dim('Example:'), chalk.cyan('cairn comment implement-auth "Found a bug in OAuth flow"'));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!message) {
|
|
118
|
+
console.error(chalk.red('Error:'), 'Missing comment message');
|
|
119
|
+
console.log(chalk.dim('Usage:'), chalk.cyan('cairn comment <task-slug> <message>'));
|
|
120
|
+
console.log(chalk.dim('Example:'), chalk.cyan('cairn comment implement-auth "Found a bug in OAuth flow"'));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Find the task to get the correct project
|
|
125
|
+
const task = findTaskBySlug(workspacePath, taskSlug, options.project);
|
|
126
|
+
|
|
127
|
+
if (!task) {
|
|
128
|
+
if (options.project) {
|
|
129
|
+
console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in project "${options.project}"`);
|
|
130
|
+
} else {
|
|
131
|
+
console.error(chalk.red('Error:'), `Task "${taskSlug}" not found in any project`);
|
|
132
|
+
console.log(chalk.dim('Tip:'), 'Use', chalk.cyan('--project <slug>'), 'to search within a specific project');
|
|
133
|
+
}
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Load auth token
|
|
138
|
+
const token = loadAuthToken(workspacePath);
|
|
139
|
+
|
|
140
|
+
if (!token) {
|
|
141
|
+
console.error(chalk.red('Error:'), 'Not authenticated. Cairn sync must be running and logged in.');
|
|
142
|
+
console.log(chalk.dim('Tip:'), 'Run', chalk.cyan('cairnsync auth login'), 'to authenticate');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Decode token to get userId
|
|
147
|
+
const payload = decodeJwtPayload(token);
|
|
148
|
+
|
|
149
|
+
if (!payload || !payload.sub) {
|
|
150
|
+
console.error(chalk.red('Error:'), 'Invalid auth token. Please re-authenticate.');
|
|
151
|
+
console.log(chalk.dim('Tip:'), 'Run', chalk.cyan('cairnsync auth login'), 'to re-authenticate');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const userId = payload.sub;
|
|
156
|
+
|
|
157
|
+
// Determine author info
|
|
158
|
+
const authorName = options.author || getAgentName();
|
|
159
|
+
const authorType = options.authorType || 'worker';
|
|
160
|
+
const authorId = authorType === 'human' ? userId : authorName.toLowerCase();
|
|
161
|
+
const commentType = options.type || 'progress';
|
|
162
|
+
|
|
163
|
+
// Build task path as Convex expects it
|
|
164
|
+
const taskPath = buildTaskPath(task.project, taskSlug);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const result = await callConvexMutation('taskComments:add', {
|
|
168
|
+
taskPath,
|
|
169
|
+
userId,
|
|
170
|
+
authorId,
|
|
171
|
+
authorType,
|
|
172
|
+
authorName,
|
|
173
|
+
content: message,
|
|
174
|
+
commentType,
|
|
175
|
+
}, token);
|
|
176
|
+
|
|
177
|
+
console.log(chalk.green('✓'), `Comment added to task: ${chalk.cyan(taskSlug)}`);
|
|
178
|
+
console.log(chalk.dim(` Project: ${task.project}`));
|
|
179
|
+
console.log(chalk.dim(` Author: ${authorName} (${authorType})`));
|
|
180
|
+
console.log(chalk.dim(` Type: ${commentType}`));
|
|
181
|
+
console.log(chalk.dim(` Message: ${message.length > 60 ? message.substring(0, 60) + '...' : message}`));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error(chalk.red('Error:'), `Failed to add comment: ${error.message}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
CHANGED