@llodev/pm-tasks-asana 1.0.0 → 1.0.1
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/CHANGELOG.md +13 -0
- package/README.md +9 -1
- package/SKILL.md +9 -7
- package/anti-patterns/asana.md +2 -2
- package/package.json +3 -3
- package/scripts/init.mjs +66 -17
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @llodev/pm-tasks-asana
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix `npx @llodev/pm-tasks-asana init` silently doing nothing. Two bugs collapsed the init flow: (a) the `bin` entry was named `pm-tasks-asana-init` (not matching the package name), so npx never resolved the binary; (b) the script's `import.meta.url === file://${process.argv[1]}` guard failed under npx's symlinked bin shim, so even when invoked the `run()` entry-point was skipped. The bin is now `pm-tasks-asana` and the entry-point runs unconditionally.
|
|
8
|
+
- Cross-platform global config path. The init prompt now honors `LLODEV_PM_TASKS_CONFIG_HOME` first, then `XDG_CONFIG_HOME` (macOS/Linux), then `%APPDATA%` (Windows), then `~/.config` as the last fallback. The prompt prints the absolute path before asking, so the destination is always visible.
|
|
9
|
+
- Slug aliases are now Unicode-aware (via `@llodev/pm-tasks-core@1.0.1`): `"Em execução"` → `"em-execucao"`, `"Média"` → `"media"` (was `"em-execu-o"` / `"m-dia"`).
|
|
10
|
+
- Drop the language-specific regex for inferring the "closed" section. The init now explicitly asks: "Which section is the default for newly-created tasks?" and "Which section means 'closed / done'?" with sensible defaults (first / last picked) and a skip option.
|
|
11
|
+
- Add an explicit escalation prompt. After the member list is collected, the init asks: "Pick the escalation contact (will receive escalation comments + add_member on critical cards)" and stores it as `defaults.escalateToAlias`. The chosen member is re-aliased to `"owner"` if no other member already holds that alias.
|
|
12
|
+
- Fallback for empty membership lists: if the PAT's scope didn't return any project members beyond `me`, the init now offers a manual entry (gid + name + alias) so single-collaborator projects can still wire an escalation contact.
|
|
13
|
+
- `multiSelect`: empty input now means "select all" instead of "select none".
|
|
14
|
+
- README clarifies the per-OS defaults and the env override.
|
|
15
|
+
|
|
3
16
|
## 1.0.0
|
|
4
17
|
|
|
5
18
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -34,7 +34,15 @@ export LLODEV_PM_TASKS_ASANA_PAT=...
|
|
|
34
34
|
npx @llodev/pm-tasks-asana init
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Walk through the prompts.
|
|
37
|
+
Walk through the prompts. Pick where the config should live:
|
|
38
|
+
|
|
39
|
+
- **local** → `./.asana.json` (recommended for project-scoped configs, can be committed).
|
|
40
|
+
- **global** → a platform default, customizable. Defaults:
|
|
41
|
+
- macOS / Linux (XDG): `$XDG_CONFIG_HOME/llodev/pm-tasks/asana.json`, falling back to `~/.config/llodev/pm-tasks/asana.json`.
|
|
42
|
+
- Windows: `%APPDATA%\llodev\pm-tasks\asana.json`.
|
|
43
|
+
- Override on any OS with `LLODEV_PM_TASKS_CONFIG_HOME=/your/path` — the file lands at `$LLODEV_PM_TASKS_CONFIG_HOME/asana.json`.
|
|
44
|
+
|
|
45
|
+
The `init` prompt prints the absolute path it will write to, so you always see exactly where the file goes.
|
|
38
46
|
|
|
39
47
|
The PAT is **only** used by `init`. The MCP itself uses OAuth — never put tokens in the JSON.
|
|
40
48
|
|
package/SKILL.md
CHANGED
|
@@ -41,11 +41,11 @@ Adapter for Asana within the `@llodev/pm-tasks-*` family. Use the core skill's e
|
|
|
41
41
|
|
|
42
42
|
## Routing
|
|
43
43
|
|
|
44
|
-
| Mode | Trigger
|
|
45
|
-
| ----------- |
|
|
46
|
-
| Paste-only | "format as Asana task" without MCP intent
|
|
47
|
-
| MCP publish | "publish to Asana", "create on Asana", "--publish-asana"
|
|
48
|
-
| Autonomous | `[autonomous]` or `--auto` in prompt OR `LLODEV_PM_TASKS_AUTONOMOUS=1`
|
|
44
|
+
| Mode | Trigger | Path |
|
|
45
|
+
| ----------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
46
|
+
| Paste-only | "format as Asana task" without MCP intent | Phase 3 (core) → Phase 4 (this skill, format only) → output paste blocks |
|
|
47
|
+
| MCP publish | "publish to Asana", "create on Asana", "--publish-asana" | Phase 3 → Phase 4 → Phase 5 (publish via MCP) |
|
|
48
|
+
| Autonomous | `[autonomous]` or `--auto` in prompt OR `LLODEV_PM_TASKS_AUTONOMOUS=1` | Phase 3 → Phase 4 → Phase 5b (write-through, no preview) |
|
|
49
49
|
| CRUD ops | "check subtask N on task X", "close task Y", "assign Alice to task Z", "comment on task X" | Phase 6 (operations, direct verb dispatch) |
|
|
50
50
|
|
|
51
51
|
## Asana model
|
|
@@ -119,7 +119,9 @@ Every verb returns the core contract shape (see [`../pm-tasks-core/references/co
|
|
|
119
119
|
"verb": "task.create",
|
|
120
120
|
"tool": "asana",
|
|
121
121
|
"ref": { "id": "<gid>", "url": "https://app.asana.com/0/<project>/<gid>", "alias": "<optional>" },
|
|
122
|
-
"details": {
|
|
122
|
+
"details": {
|
|
123
|
+
/* Asana-specific (see table below) */
|
|
124
|
+
}
|
|
123
125
|
}
|
|
124
126
|
```
|
|
125
127
|
|
|
@@ -142,7 +144,7 @@ See [`anti-patterns/asana.md`](anti-patterns/asana.md) — paste health, custom-
|
|
|
142
144
|
|
|
143
145
|
## Standalone fallback
|
|
144
146
|
|
|
145
|
-
If `@llodev/pm-tasks-core` is not installed: ask the user for minimum input (title + subtask names) and produce a paste-ready Asana task body from this content alone. Quality is degraded — no scope/audience/fidelity inference. Print:
|
|
147
|
+
If `@llodev/pm-tasks-core` is not installed: ask the user for minimum input (title + subtask names) and produce a paste-ready Asana task body from this content alone. Quality is degraded — no scope/audience/fidelity inference. Print: _"Install `@llodev/pm-tasks-core` for the full flow."_
|
|
146
148
|
|
|
147
149
|
## Config
|
|
148
150
|
|
package/anti-patterns/asana.md
CHANGED
|
@@ -6,8 +6,8 @@ Apply when Phase 4 / Phase 5 / Phase 5b target Asana. Authoritative formatting +
|
|
|
6
6
|
|
|
7
7
|
## Paste health and fallbacks
|
|
8
8
|
|
|
9
|
-
| Healthy paste
|
|
10
|
-
|
|
|
9
|
+
| Healthy paste | If it degrades → fallback |
|
|
10
|
+
| --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
11
11
|
| Prefer `**Section**` in task descriptions when `##` headings are unreliable; **only one** nesting level for subtasks. | Swap heading hierarchy for bold labels; split deep trees into sibling tasks. See [`../SKILL.md`](../SKILL.md) §Asana model. |
|
|
12
12
|
|
|
13
13
|
**Switching tools mid-thread:** re-load the new tool's adapter; do not carry Asana rules into Trello / Jira / Linear (see **Cross-tool** below).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llodev/pm-tasks-asana",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Asana adapter for the @llodev/pm-tasks-* family. Use when the user mentions Asana (create Asana task, publish to Asana, post to Asana, add comment in Asana, --publish-asana, close task, check subtask) or wants to publish a plan as Asana tasks with subtasks. Modes: paste-ready (no MCP needed), MCP publish (via claude.ai Asana MCP), autonomous (sentinel [autonomous] / --auto). Implements 6 CRUD verbs from @llodev/pm-tasks-core/references/contract.md mapped to Asana (parent task + subtasks, custom fields, sections, multi-assignee). REQUIRES: @llodev/pm-tasks-core installed (skillpm / Claude Code marketplace cascade auto; Vercel CLI users install manually).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/llodev/skills/tree/main/pm-tasks/pm-tasks-asana",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
],
|
|
29
29
|
"type": "module",
|
|
30
30
|
"bin": {
|
|
31
|
-
"pm-tasks-asana
|
|
31
|
+
"pm-tasks-asana": "./scripts/init.mjs"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@llodev/pm-tasks-core": "^1.0.
|
|
34
|
+
"@llodev/pm-tasks-core": "^1.0.1"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
package/scripts/init.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
promptScope,
|
|
8
8
|
promptYesNo,
|
|
9
9
|
multiSelect,
|
|
10
|
+
promptPick,
|
|
11
|
+
aliasOf,
|
|
10
12
|
writeConfig,
|
|
11
13
|
validateConfig,
|
|
12
14
|
probeMCP,
|
|
@@ -39,8 +41,7 @@ async function asanaProbe() {
|
|
|
39
41
|
getWorkspaces: () => j("/workspaces?opt_fields=name"),
|
|
40
42
|
getProjects: (workspaceGid) =>
|
|
41
43
|
j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
|
|
42
|
-
getSections: (projectGid) =>
|
|
43
|
-
j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
|
|
44
|
+
getSections: (projectGid) => j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
|
|
44
45
|
getCustomFields: (projectGid) =>
|
|
45
46
|
j(
|
|
46
47
|
`/projects/${projectGid}/custom_field_settings?opt_fields=custom_field.name,custom_field.gid,custom_field.resource_subtype,custom_field.enum_options.name,custom_field.enum_options.gid&limit=100`,
|
|
@@ -50,11 +51,46 @@ async function asanaProbe() {
|
|
|
50
51
|
};
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
async function promptManualMember() {
|
|
55
|
+
const { createInterface } = await import("node:readline/promises");
|
|
56
|
+
const { stdin: input, stdout: output } = await import("node:process");
|
|
57
|
+
const r = createInterface({ input, output });
|
|
58
|
+
try {
|
|
59
|
+
const gid = (await r.question("Escalation member gid (Asana user ID): ")).trim();
|
|
60
|
+
if (!gid) return null;
|
|
61
|
+
const name = (await r.question("Display name: ")).trim() || "owner";
|
|
62
|
+
const alias = (await r.question(`Alias (default "owner"): `)).trim() || "owner";
|
|
63
|
+
return { id: gid, name, alias };
|
|
64
|
+
} finally {
|
|
65
|
+
r.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function collectEscalationMember(out) {
|
|
70
|
+
const candidates = out.members.filter((m) => m.alias !== "me");
|
|
71
|
+
if (candidates.length) {
|
|
72
|
+
const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
|
|
73
|
+
const picked = await promptPick(
|
|
74
|
+
"Pick the escalation contact (will receive escalation comments + add_member on critical cards):",
|
|
75
|
+
choices,
|
|
76
|
+
{ defaultIndex: 0, allowSkip: true },
|
|
77
|
+
);
|
|
78
|
+
if (picked) {
|
|
79
|
+
if (picked.alias !== "owner") {
|
|
80
|
+
if (!out.members.find((m) => m.alias === "owner")) picked.alias = "owner";
|
|
81
|
+
}
|
|
82
|
+
out.defaults.escalateToAlias = picked.alias;
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const add = await promptYesNo(
|
|
87
|
+
"Project membership listing returned nothing (PAT scope or solo project). Add an escalation contact manually?",
|
|
88
|
+
);
|
|
89
|
+
if (!add) return;
|
|
90
|
+
const manual = await promptManualMember();
|
|
91
|
+
if (!manual) return;
|
|
92
|
+
out.members.push(manual);
|
|
93
|
+
out.defaults.escalateToAlias = manual.alias;
|
|
58
94
|
}
|
|
59
95
|
|
|
60
96
|
function mapResourceType(subtype) {
|
|
@@ -212,13 +248,28 @@ async function run() {
|
|
|
212
248
|
|
|
213
249
|
if (out.projects.length === 1) {
|
|
214
250
|
out.defaults.projectAlias = out.projects[0].alias;
|
|
215
|
-
const backlog = out.sections.find((s) => /backlog|todo|to.do|inbox/i.test(s.name));
|
|
216
|
-
const done = out.sections.find((s) => /done|completed|published|conclu/i.test(s.name));
|
|
217
|
-
if (backlog) out.defaults.sectionAlias = backlog.alias;
|
|
218
|
-
if (done) out.defaults.closeSectionAlias = done.alias;
|
|
219
251
|
out.defaults.assigneeAlias = "me";
|
|
252
|
+
const sectionChoices = out.sections.map((s) => ({
|
|
253
|
+
label: `${s.name} (${s.alias})`,
|
|
254
|
+
value: s,
|
|
255
|
+
}));
|
|
256
|
+
if (sectionChoices.length) {
|
|
257
|
+
const open = await promptPick(
|
|
258
|
+
"Which section is the default for newly-created tasks?",
|
|
259
|
+
sectionChoices,
|
|
260
|
+
{ defaultIndex: 0, allowSkip: true },
|
|
261
|
+
);
|
|
262
|
+
if (open) out.defaults.sectionAlias = open.alias;
|
|
263
|
+
const close = await promptPick("Which section means 'closed / done'?", sectionChoices, {
|
|
264
|
+
defaultIndex: sectionChoices.length - 1,
|
|
265
|
+
allowSkip: true,
|
|
266
|
+
});
|
|
267
|
+
if (close) out.defaults.closeSectionAlias = close.alias;
|
|
268
|
+
}
|
|
220
269
|
}
|
|
221
270
|
|
|
271
|
+
await collectEscalationMember(out);
|
|
272
|
+
|
|
222
273
|
if (inheritFieldIds.size) {
|
|
223
274
|
out.subtaskDefaults = {
|
|
224
275
|
inheritParentFields: [...inheritFieldIds],
|
|
@@ -262,9 +313,7 @@ async function run() {
|
|
|
262
313
|
]);
|
|
263
314
|
}
|
|
264
315
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
});
|
|
270
|
-
}
|
|
316
|
+
run().catch((e) => {
|
|
317
|
+
console.error(e);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
});
|