@llodev/pm-tasks-asana 1.3.0 → 1.4.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/CHANGELOG.md +63 -0
- package/README.md +2 -2
- package/SKILL.md +20 -26
- package/dist/bin/init.d.ts +3 -0
- package/dist/bin/init.d.ts.map +1 -0
- package/dist/bin/init.js +274 -0
- package/dist/bin/init.js.map +1 -0
- package/{README.es-ES.md → docs/i18n/README.es-ES.md} +9 -9
- package/{README.pt-BR.md → docs/i18n/README.pt-BR.md} +9 -9
- package/manifest.json +1 -0
- package/package.json +28 -13
- package/references/operations.md +62 -0
- package/SKILL.es-ES.md +0 -170
- package/SKILL.pt-BR.md +0 -170
- package/i18n/parity.test.mjs +0 -33
- package/scripts/init.mjs +0 -319
package/scripts/init.mjs
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// pm-tasks-asana init — interactive config bootstrapper
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
promptScope,
|
|
8
|
-
promptYesNo,
|
|
9
|
-
multiSelect,
|
|
10
|
-
promptPick,
|
|
11
|
-
aliasOf,
|
|
12
|
-
writeConfig,
|
|
13
|
-
validateConfig,
|
|
14
|
-
probeMCP,
|
|
15
|
-
printInstructions,
|
|
16
|
-
promptLocale,
|
|
17
|
-
loadStrings,
|
|
18
|
-
registerI18nRoot,
|
|
19
|
-
interpolate,
|
|
20
|
-
} from "@llodev/pm-tasks-core/init-lib";
|
|
21
|
-
|
|
22
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
-
|
|
24
|
-
async function loadSchema() {
|
|
25
|
-
const raw = await readFile(path.join(ROOT, "schemas", "config.json"), "utf8");
|
|
26
|
-
return JSON.parse(raw);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function asanaProbe() {
|
|
30
|
-
// The MCP runs in a different process. To probe within this script, call the
|
|
31
|
-
// Asana REST API directly with a Personal Access Token.
|
|
32
|
-
// Used only to enumerate workspaces / projects / sections / custom fields / members.
|
|
33
|
-
const TOKEN = process.env.LLODEV_PM_TASKS_ASANA_PAT;
|
|
34
|
-
if (!TOKEN) throw new Error("auth: LLODEV_PM_TASKS_ASANA_PAT missing");
|
|
35
|
-
const j = async (p) => {
|
|
36
|
-
const r = await fetch(`https://app.asana.com/api/1.0${p}`, {
|
|
37
|
-
headers: { Authorization: `Bearer ${TOKEN}` },
|
|
38
|
-
});
|
|
39
|
-
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
40
|
-
const body = await r.json();
|
|
41
|
-
return body.data;
|
|
42
|
-
};
|
|
43
|
-
return {
|
|
44
|
-
getMe: () => j("/users/me?opt_fields=gid,name,email"),
|
|
45
|
-
getWorkspaces: () => j("/workspaces?opt_fields=name"),
|
|
46
|
-
getProjects: (workspaceGid) =>
|
|
47
|
-
j(`/projects?workspace=${workspaceGid}&opt_fields=name&limit=100`),
|
|
48
|
-
getSections: (projectGid) => j(`/projects/${projectGid}/sections?opt_fields=name&limit=100`),
|
|
49
|
-
getCustomFields: (projectGid) =>
|
|
50
|
-
j(
|
|
51
|
-
`/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`,
|
|
52
|
-
),
|
|
53
|
-
getMembers: (projectGid) =>
|
|
54
|
-
j(`/projects/${projectGid}/members?opt_fields=user.name,user.gid&limit=100`),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function promptManualMember(asanaStrings) {
|
|
59
|
-
const { createInterface } = await import("node:readline/promises");
|
|
60
|
-
const { stdin: input, stdout: output } = await import("node:process");
|
|
61
|
-
const r = createInterface({ input, output });
|
|
62
|
-
try {
|
|
63
|
-
const gid = (await r.question(asanaStrings.manualMemberGid)).trim();
|
|
64
|
-
if (!gid) return null;
|
|
65
|
-
const name = (await r.question(asanaStrings.manualMemberName)).trim() || "owner";
|
|
66
|
-
const alias = (await r.question(asanaStrings.manualMemberAlias)).trim() || "owner";
|
|
67
|
-
return { id: gid, name, alias };
|
|
68
|
-
} finally {
|
|
69
|
-
r.close();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function collectEscalationMember(out, { coreStrings, asanaStrings }) {
|
|
74
|
-
const candidates = out.members.filter((m) => m.alias !== "me");
|
|
75
|
-
if (candidates.length) {
|
|
76
|
-
const choices = candidates.map((m) => ({ label: `${m.name} (${m.alias})`, value: m }));
|
|
77
|
-
const picked = await promptPick(asanaStrings.escalationPrompt, choices, {
|
|
78
|
-
defaultIndex: 0,
|
|
79
|
-
allowSkip: true,
|
|
80
|
-
strings: coreStrings,
|
|
81
|
-
});
|
|
82
|
-
if (picked) {
|
|
83
|
-
if (picked.alias !== "owner") {
|
|
84
|
-
if (!out.members.find((m) => m.alias === "owner")) picked.alias = "owner";
|
|
85
|
-
}
|
|
86
|
-
out.defaults.escalateToAlias = picked.alias;
|
|
87
|
-
}
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const add = await promptYesNo(asanaStrings.noMembersPrompt, { strings: coreStrings });
|
|
91
|
-
if (!add) return;
|
|
92
|
-
const manual = await promptManualMember(asanaStrings);
|
|
93
|
-
if (!manual) return;
|
|
94
|
-
out.members.push(manual);
|
|
95
|
-
out.defaults.escalateToAlias = manual.alias;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function mapResourceType(subtype) {
|
|
99
|
-
switch (subtype) {
|
|
100
|
-
case "enum":
|
|
101
|
-
return "enum";
|
|
102
|
-
case "multi_enum":
|
|
103
|
-
return "multi_enum";
|
|
104
|
-
case "number":
|
|
105
|
-
return "number";
|
|
106
|
-
case "date":
|
|
107
|
-
return "date";
|
|
108
|
-
case "people":
|
|
109
|
-
return "people";
|
|
110
|
-
default:
|
|
111
|
-
return "text";
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function run() {
|
|
116
|
-
registerI18nRoot("asana", path.join(ROOT, "i18n"));
|
|
117
|
-
const locale = await promptLocale("core", { defaultLocale: "en-US" });
|
|
118
|
-
const coreStrings = await loadStrings("core", locale);
|
|
119
|
-
const asanaStrings = await loadStrings("asana", locale);
|
|
120
|
-
console.log(`\n${asanaStrings.header}\n`);
|
|
121
|
-
|
|
122
|
-
const { path: outPath } = await promptScope("asana", { strings: coreStrings });
|
|
123
|
-
|
|
124
|
-
const probe = await probeMCP({
|
|
125
|
-
tool: "asana",
|
|
126
|
-
probeCommand: async () => {
|
|
127
|
-
const api = await asanaProbe();
|
|
128
|
-
await api.getMe();
|
|
129
|
-
return api;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
if (probe.unauthenticated) {
|
|
134
|
-
printInstructions([asanaStrings.patMissingTitle, asanaStrings.patMissingBody]);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!probe.mcpAvailable) {
|
|
139
|
-
printInstructions([asanaStrings.mcpMissingTitle, asanaStrings.mcpMissingBody]);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const api = probe.result;
|
|
144
|
-
const me = await api.getMe();
|
|
145
|
-
const workspaces = await api.getWorkspaces();
|
|
146
|
-
|
|
147
|
-
const pickedWorkspaces = await multiSelect(
|
|
148
|
-
asanaStrings.workspacePrompt,
|
|
149
|
-
workspaces.map((w) => ({ label: `${w.name} (${w.gid})`, value: w })),
|
|
150
|
-
{ strings: coreStrings },
|
|
151
|
-
);
|
|
152
|
-
if (!pickedWorkspaces.length) {
|
|
153
|
-
console.error(asanaStrings.noWorkspace);
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
const workspace = pickedWorkspaces[0];
|
|
157
|
-
|
|
158
|
-
const projects = await api.getProjects(workspace.gid);
|
|
159
|
-
const pickedProjects = await multiSelect(
|
|
160
|
-
interpolate(asanaStrings.projectsPrompt, { workspace: workspace.name }),
|
|
161
|
-
projects.map((p) => ({ label: `${p.name} (${p.gid})`, value: p })),
|
|
162
|
-
{ strings: coreStrings },
|
|
163
|
-
);
|
|
164
|
-
if (!pickedProjects.length) {
|
|
165
|
-
console.error(asanaStrings.noProject);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const out = {
|
|
170
|
-
$schema: "https://llodev.github.io/skills/schemas/pm-tasks-asana.json",
|
|
171
|
-
version: "1",
|
|
172
|
-
locale,
|
|
173
|
-
workspace: { id: workspace.gid, name: workspace.name },
|
|
174
|
-
projects: [],
|
|
175
|
-
sections: [],
|
|
176
|
-
customFields: [],
|
|
177
|
-
members: [{ id: me.gid, name: me.name, email: me.email, alias: "me" }],
|
|
178
|
-
defaults: {},
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const inheritFieldIds = new Set();
|
|
182
|
-
|
|
183
|
-
for (const p of pickedProjects) {
|
|
184
|
-
const alias = aliasOf(p.name);
|
|
185
|
-
out.projects.push({ id: p.gid, name: p.name, alias });
|
|
186
|
-
|
|
187
|
-
const sections = await api.getSections(p.gid);
|
|
188
|
-
const pickedSections = await multiSelect(
|
|
189
|
-
interpolate(asanaStrings.sectionsPrompt, { project: p.name }),
|
|
190
|
-
sections.map((s) => ({ label: s.name, value: s })),
|
|
191
|
-
{ strings: coreStrings },
|
|
192
|
-
);
|
|
193
|
-
for (const s of pickedSections) {
|
|
194
|
-
out.sections.push({
|
|
195
|
-
projectAlias: alias,
|
|
196
|
-
id: s.gid,
|
|
197
|
-
name: s.name,
|
|
198
|
-
alias: aliasOf(s.name),
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const cfSettings = await api.getCustomFields(p.gid);
|
|
203
|
-
const fields = cfSettings.map((cs) => cs.custom_field).filter(Boolean);
|
|
204
|
-
if (fields.length) {
|
|
205
|
-
const pickedFields = await multiSelect(
|
|
206
|
-
interpolate(asanaStrings.customFieldsPrompt, { project: p.name }),
|
|
207
|
-
fields.map((f) => ({ label: `${f.name} [${f.resource_subtype}]`, value: f })),
|
|
208
|
-
{ strings: coreStrings },
|
|
209
|
-
);
|
|
210
|
-
for (const f of pickedFields) {
|
|
211
|
-
const entry = {
|
|
212
|
-
projectAlias: alias,
|
|
213
|
-
id: f.gid,
|
|
214
|
-
name: f.name,
|
|
215
|
-
type: mapResourceType(f.resource_subtype),
|
|
216
|
-
alias: aliasOf(f.name),
|
|
217
|
-
};
|
|
218
|
-
if (Array.isArray(f.enum_options) && f.enum_options.length) {
|
|
219
|
-
entry.options = f.enum_options.map((opt) => ({
|
|
220
|
-
id: opt.gid,
|
|
221
|
-
name: opt.name,
|
|
222
|
-
alias: aliasOf(opt.name),
|
|
223
|
-
}));
|
|
224
|
-
}
|
|
225
|
-
out.customFields.push(entry);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (pickedFields.length) {
|
|
229
|
-
const inheritPicked = await multiSelect(
|
|
230
|
-
asanaStrings.inheritPrompt,
|
|
231
|
-
pickedFields.map((f) => ({ label: f.name, value: f })),
|
|
232
|
-
{ strings: coreStrings },
|
|
233
|
-
);
|
|
234
|
-
for (const f of inheritPicked) inheritFieldIds.add(f.gid);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const memberships = await api.getMembers(p.gid);
|
|
240
|
-
const users = memberships.map((m) => m.user).filter(Boolean);
|
|
241
|
-
for (const u of users) {
|
|
242
|
-
if (u.gid === me.gid) continue;
|
|
243
|
-
if (out.members.find((m) => m.id === u.gid)) continue;
|
|
244
|
-
out.members.push({ id: u.gid, name: u.name, alias: aliasOf(u.name) });
|
|
245
|
-
}
|
|
246
|
-
} catch (e) {
|
|
247
|
-
// Project membership listing may require additional scopes; skip silently.
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (out.projects.length === 1) {
|
|
252
|
-
out.defaults.projectAlias = out.projects[0].alias;
|
|
253
|
-
out.defaults.assigneeAlias = "me";
|
|
254
|
-
const sectionChoices = out.sections.map((s) => ({
|
|
255
|
-
label: `${s.name} (${s.alias})`,
|
|
256
|
-
value: s,
|
|
257
|
-
}));
|
|
258
|
-
if (sectionChoices.length) {
|
|
259
|
-
const open = await promptPick(asanaStrings.openSectionPrompt, sectionChoices, {
|
|
260
|
-
defaultIndex: 0,
|
|
261
|
-
allowSkip: true,
|
|
262
|
-
strings: coreStrings,
|
|
263
|
-
});
|
|
264
|
-
if (open) out.defaults.sectionAlias = open.alias;
|
|
265
|
-
const close = await promptPick(asanaStrings.closeSectionPrompt, sectionChoices, {
|
|
266
|
-
defaultIndex: sectionChoices.length - 1,
|
|
267
|
-
allowSkip: true,
|
|
268
|
-
strings: coreStrings,
|
|
269
|
-
});
|
|
270
|
-
if (close) out.defaults.closeSectionAlias = close.alias;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
await collectEscalationMember(out, { coreStrings, asanaStrings });
|
|
275
|
-
|
|
276
|
-
if (inheritFieldIds.size) {
|
|
277
|
-
out.subtaskDefaults = {
|
|
278
|
-
inheritParentFields: [...inheritFieldIds],
|
|
279
|
-
inheritAssignee: true,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const wantAuto = await promptYesNo(coreStrings.autonomousPromptQuestion, {
|
|
284
|
-
strings: coreStrings,
|
|
285
|
-
});
|
|
286
|
-
if (wantAuto) {
|
|
287
|
-
out.autonomous = {
|
|
288
|
-
enabled: false,
|
|
289
|
-
allow: ["task.create", "checklist.check", "task.close", "task.comment.add"],
|
|
290
|
-
scope: {
|
|
291
|
-
projects: out.projects.map((p) => p.id),
|
|
292
|
-
sections: out.sections.map((s) => s.id),
|
|
293
|
-
},
|
|
294
|
-
rateLimit: { writesPerMinute: 30, commentsPerMinute: 10 },
|
|
295
|
-
auditLog: "~/.local/share/llodev/pm-tasks/asana/audit.log",
|
|
296
|
-
};
|
|
297
|
-
printInstructions([coreStrings.autonomousAddedTitle, coreStrings.autonomousReviewBody]);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const schema = await loadSchema();
|
|
301
|
-
const valid = await validateConfig(out, schema);
|
|
302
|
-
if (!valid.ok) {
|
|
303
|
-
console.error(`${coreStrings.errInvalidConfig}:`, JSON.stringify(valid.errors, null, 2));
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
await writeConfig(outPath, out);
|
|
308
|
-
printInstructions([
|
|
309
|
-
interpolate(coreStrings.configWritten, { path: outPath }),
|
|
310
|
-
asanaStrings.tryItTitle,
|
|
311
|
-
asanaStrings.tokenReminderTitle,
|
|
312
|
-
asanaStrings.tokenReminderBody,
|
|
313
|
-
]);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
run().catch((e) => {
|
|
317
|
-
console.error(e);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
});
|