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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cairn-work",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "AI-native project management - optimized CLI for AI agents and humans working together",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@ Things like:
18
18
  - front-door → Entrance, motion-triggered
19
19
 
20
20
  ### SSH
21
- - home-server → 192.168.1.100, user: admin
21
+ - home-server → 10.0.0.1, user: deploy
22
22
 
23
23
  ### Cairn
24
24
  - Workspace: {{WORKSPACE_PATH}}