asana-cli-skill 1.2.0 → 1.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 CHANGED
@@ -189,7 +189,7 @@ Line 2"
189
189
 
190
190
  ### `asana-cli update <url-or-id>`
191
191
 
192
- Updates a task: `--name`, `--notes` (replace), `--append-notes` (add to the end), `--assignee` (`none` unassigns), `--due` (`none` clears), `--complete` or `--reopen`, and custom fields with `--field "Name=Value"` (repeatable). Enum options match by name ignoring case, accents and icons, so `--field "Priority=Urgent"` selects an option shown with a leading icon.
192
+ Updates a task: `--name`, `--notes` (replace), `--append-notes` (add to the end), `--assignee` (`none` unassigns), `--due` (`none` clears), `--complete` or `--reopen`, custom fields with `--field "Name=Value"` (repeatable), and images shown inline at the end of the description with `--image <path>` (repeatable; the text is kept unless `--notes` replaces it). Enum options match by name ignoring case, accents and icons, so `--field "Priority=Urgent"` selects an option shown with a leading icon.
193
193
 
194
194
  ```bash
195
195
  asana-cli update 5678 --due 2026-10-31 --append-notes "Blocked by the vendor"
package/bin/asana-cli.js CHANGED
@@ -189,6 +189,7 @@ program
189
189
  .option('--complete', 'Mark as completed (without a comment)')
190
190
  .option('--reopen', 'Mark as not completed')
191
191
  .option('--field <name=value>', 'Set a custom field, e.g. "Priority=High" (repeatable)', collectMultiple, [])
192
+ .option('--image <path>', 'Attach an image and show it inline at the end of the description (repeatable)', collectMultiple, [])
192
193
  .action(async (urlOrId, options) => {
193
194
  await updateCommand(urlOrId, options, getClient());
194
195
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "asana-cli-skill",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Minimal CLI for Asana task management, built to power a Claude Code skill",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  "url": "https://github.com/giacomeli/asana-cli-skill/issues"
15
15
  },
16
16
  "bin": {
17
- "asana-cli": "./bin/asana-cli.js"
17
+ "asana-cli": "bin/asana-cli.js"
18
18
  },
19
19
  "files": [
20
20
  "bin",
@@ -144,7 +144,7 @@ Quick reference for `asana-cli`:
144
144
  | `asana-cli my-tasks` | Open tasks assigned to the user |
145
145
  | `asana-cli comments <url-or-id> [--last n]` | Read a task's comments |
146
146
  | `asana-cli create-task -n "name" -p <project> [-s section]` | Create a task |
147
- | `asana-cli update <id> [--name/--notes/--append-notes/--assignee/--due/--complete/--reopen/--field "Name=Value"]` | Update a task, including custom fields |
147
+ | `asana-cli update <id> [--name/--notes/--append-notes/--assignee/--due/--complete/--reopen/--field "Name=Value"/--image <path>]` | Update a task, including custom fields and inline images in the description |
148
148
  | `asana-cli move <id> --after <sibling-id>` | Reorder subtasks or change the parent |
149
149
  | `asana-cli edit-comment <comment-id> -m "text"` | Fix a comment's text, keeping inline images |
150
150
  | `asana-cli delete-comment <comment-id>` | Delete a comment (ID from `comments`) |
@@ -1,4 +1,6 @@
1
- import { parseTaskId, buildTaskFields, unescapeNewlines, parseFieldAssignments } from '../utils.js';
1
+ import { existsSync } from 'fs';
2
+
3
+ import { parseTaskId, buildTaskFields, unescapeNewlines, parseFieldAssignments, buildHtmlNotes } from '../utils.js';
2
4
  import { formatSuccess } from '../formatter.js';
3
5
 
4
6
  export async function updateCommand(urlOrId, options, client) {
@@ -11,6 +13,11 @@ export async function updateCommand(urlOrId, options, client) {
11
13
  throw new Error('Use either --complete or --reopen, not both');
12
14
  }
13
15
 
16
+ const images = options.image || [];
17
+ for (const filePath of images) {
18
+ if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
19
+ }
20
+
14
21
  const data = buildTaskFields(options);
15
22
 
16
23
  if (options.appendNotes !== undefined) {
@@ -23,8 +30,19 @@ export async function updateCommand(urlOrId, options, client) {
23
30
 
24
31
  const fields = parseFieldAssignments(options.field || []);
25
32
 
26
- if (Object.keys(data).length === 0 && fields.length === 0) {
27
- throw new Error('Nothing to update. Pass at least one of --name, --notes, --append-notes, --assignee, --due, --complete, --reopen, --field');
33
+ if (Object.keys(data).length === 0 && fields.length === 0 && images.length === 0) {
34
+ throw new Error('Nothing to update. Pass at least one of --name, --notes, --append-notes, --assignee, --due, --complete, --reopen, --field, --image');
35
+ }
36
+
37
+ // Images go inline after the description text, which is kept when --notes is not passed.
38
+ if (images.length > 0) {
39
+ const text = data.notes ?? (await client.getTask(taskId)).notes ?? '';
40
+ delete data.notes;
41
+ const gids = [];
42
+ for (const filePath of images) {
43
+ gids.push((await client.uploadAttachment(taskId, filePath)).gid);
44
+ }
45
+ data.html_notes = buildHtmlNotes(text, gids);
28
46
  }
29
47
 
30
48
  if (Object.keys(data).length > 0) {
package/src/utils.js CHANGED
@@ -136,3 +136,13 @@ export function parseFieldAssignments(pairs) {
136
136
  return { name: pair.slice(0, index).trim(), value: pair.slice(index + 1).trim() };
137
137
  });
138
138
  }
139
+
140
+ /**
141
+ * Builds a rich-text description: the plain text (escaped) followed by the task's own
142
+ * attachments shown inline. Asana only renders <img data-asana-gid> for attachments of the same task.
143
+ */
144
+ export function buildHtmlNotes(text, imageGids) {
145
+ const escaped = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
146
+ const images = imageGids.map((gid) => `<img data-asana-gid="${gid}"/>`).join('');
147
+ return `<body>${escaped ? `${escaped}\n` : ''}${images}</body>`;
148
+ }