@gethmy/mcp 3.2.0 → 3.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 +2 -2
- package/dist/cli.js +209 -12
- package/dist/index.js +208 -12
- package/dist/lib/api-client.js +3 -1
- package/package.json +1 -1
- package/src/api-client.ts +31 -0
- package/src/comment-session.ts +149 -0
- package/src/plan-task-link.ts +128 -0
- package/src/server.ts +260 -32
- package/src/tui/setup.ts +3 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Claude Code, OpenAI Codex, Cursor, and any MCP client claim cards, report progre
|
|
|
5
5
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
|
-
- **
|
|
8
|
+
- **79 MCP Tools** for full board control, knowledge graph, and workflow plans
|
|
9
9
|
- **Global Skills** — installable in one command, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
|
|
10
10
|
- **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
|
|
11
11
|
- **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
|
|
@@ -89,7 +89,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
|
|
|
89
89
|
1. Get an API key from [Harmony](https://gethmy.com/user/keys)
|
|
90
90
|
2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
|
|
91
91
|
3. Set the Authorization header to `Bearer hmy_your_key_here`
|
|
92
|
-
4. All
|
|
92
|
+
4. All 79 Harmony tools become available in your conversation
|
|
93
93
|
|
|
94
94
|
**Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
|
|
95
95
|
|
package/dist/cli.js
CHANGED
|
@@ -1389,7 +1389,6 @@ var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
|
1389
1389
|
var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1390
1390
|
var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1391
1391
|
var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1392
|
-
var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1393
1392
|
var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1394
1393
|
// ../harmony-shared/dist/cardLinks.js
|
|
1395
1394
|
var LINK_TYPE_INVERSES = {
|
|
@@ -2617,6 +2616,9 @@ ${untrustedDataBlock(planContent.trim(), {
|
|
|
2617
2616
|
async updatePlaybook(playbookId, updates) {
|
|
2618
2617
|
return this.request("PATCH", `/playbooks/${playbookId}`, updates);
|
|
2619
2618
|
}
|
|
2619
|
+
async deletePlaybook(playbookId) {
|
|
2620
|
+
return this.request("DELETE", `/playbooks/${encodeURIComponent(playbookId)}`);
|
|
2621
|
+
}
|
|
2620
2622
|
}
|
|
2621
2623
|
var _promptModules = null;
|
|
2622
2624
|
async function loadPromptModules() {
|
|
@@ -2854,6 +2856,36 @@ async function autoEndSession(scope, client3, cardId, status) {
|
|
|
2854
2856
|
} catch {}
|
|
2855
2857
|
}
|
|
2856
2858
|
|
|
2859
|
+
// src/comment-session.ts
|
|
2860
|
+
var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
|
|
2861
|
+
var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
|
|
2862
|
+
function readDeclaredRunSession(env = process.env) {
|
|
2863
|
+
const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
|
|
2864
|
+
const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
|
|
2865
|
+
if (!cardId || !agentSessionId)
|
|
2866
|
+
return null;
|
|
2867
|
+
return { cardId, agentSessionId };
|
|
2868
|
+
}
|
|
2869
|
+
function chooseCommentSession(args) {
|
|
2870
|
+
const tracked = args.tracked;
|
|
2871
|
+
if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
|
|
2872
|
+
return {
|
|
2873
|
+
kind: "session",
|
|
2874
|
+
agentSessionId: tracked.agentSessionId,
|
|
2875
|
+
source: "tracked"
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
const declared = args.declared;
|
|
2879
|
+
if (declared && declared.cardId === args.cardId) {
|
|
2880
|
+
return {
|
|
2881
|
+
kind: "session",
|
|
2882
|
+
agentSessionId: declared.agentSessionId,
|
|
2883
|
+
source: "declared"
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
return { kind: "sessionless" };
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2857
2889
|
// src/server.ts
|
|
2858
2890
|
init_config();
|
|
2859
2891
|
|
|
@@ -3452,6 +3484,51 @@ async function onboardNewUser(params) {
|
|
|
3452
3484
|
};
|
|
3453
3485
|
}
|
|
3454
3486
|
|
|
3487
|
+
// src/plan-task-link.ts
|
|
3488
|
+
function findPlanTask(tasks, taskId) {
|
|
3489
|
+
if (!taskId) {
|
|
3490
|
+
return { ok: false, reason: "No plan task id was given." };
|
|
3491
|
+
}
|
|
3492
|
+
if (!Array.isArray(tasks)) {
|
|
3493
|
+
return {
|
|
3494
|
+
ok: false,
|
|
3495
|
+
reason: "The plan returned no readable criteria list."
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
for (const row of tasks) {
|
|
3499
|
+
if (!row || typeof row !== "object")
|
|
3500
|
+
continue;
|
|
3501
|
+
const candidate = row;
|
|
3502
|
+
if (candidate.id === taskId) {
|
|
3503
|
+
return { ok: true, task: candidate };
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
return {
|
|
3507
|
+
ok: false,
|
|
3508
|
+
reason: `Plan task ${taskId} is not one of this plan's criteria. ` + `Read the plan with harmony_get_plan and use an id from its \`tasks\`.`
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
function linkedReport(planId, task, newCardId) {
|
|
3512
|
+
const previous = task.card_id ?? null;
|
|
3513
|
+
return {
|
|
3514
|
+
planId,
|
|
3515
|
+
taskId: task.id,
|
|
3516
|
+
linked: true,
|
|
3517
|
+
criterion: task.content ?? null,
|
|
3518
|
+
...previous && previous !== newCardId ? { replacedCardId: previous } : {}
|
|
3519
|
+
};
|
|
3520
|
+
}
|
|
3521
|
+
function unlinkedReport(planId, task, error) {
|
|
3522
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3523
|
+
return {
|
|
3524
|
+
planId,
|
|
3525
|
+
taskId: task.id,
|
|
3526
|
+
linked: false,
|
|
3527
|
+
criterion: task.content ?? null,
|
|
3528
|
+
error: `The card was created, but the plan criterion still does not point at it: ${message}. ` + `Repair it with harmony_link_plan_task — do not create the card again.`
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3455
3532
|
// src/playbook-metric-warnings.ts
|
|
3456
3533
|
function playbookMetricWarnings(agents, steps) {
|
|
3457
3534
|
if (!Array.isArray(steps))
|
|
@@ -3949,12 +4026,13 @@ function optionalNonNegativeNumberArg(raw, field) {
|
|
|
3949
4026
|
throw new Error(`${field} must be a non-negative number.`);
|
|
3950
4027
|
return n;
|
|
3951
4028
|
}
|
|
3952
|
-
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
|
|
4029
|
+
function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, scopeId) {
|
|
3953
4030
|
memorySessions.set(cardId, {
|
|
3954
4031
|
cardId,
|
|
3955
4032
|
agentIdentifier,
|
|
3956
4033
|
agentName,
|
|
3957
4034
|
agentSessionId,
|
|
4035
|
+
scopeId,
|
|
3958
4036
|
memoryReadCount: 0,
|
|
3959
4037
|
pendingActions: [],
|
|
3960
4038
|
allActions: [],
|
|
@@ -4066,6 +4144,10 @@ var TOOLS = {
|
|
|
4066
4144
|
type: "string",
|
|
4067
4145
|
description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
|
|
4068
4146
|
},
|
|
4147
|
+
planTaskId: {
|
|
4148
|
+
type: "string",
|
|
4149
|
+
description: "Id of the plan CRITERION this card is created to fulfil (optional; requires `planId`). " + "Sets both directions at once: the card's plan_id, and the criterion's card_id — the " + "return leg a card outcome needs to reach the plan. Read the ids from harmony_get_plan's " + "`tasks`. A criterion that is not in the named plan refuses the whole call, so no card is " + "created on a false premise; a failure to write the return leg AFTER the card exists " + "keeps the card and reports it in `planTask` instead."
|
|
4150
|
+
},
|
|
4069
4151
|
attachments: {
|
|
4070
4152
|
type: "array",
|
|
4071
4153
|
description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
|
|
@@ -5543,6 +5625,29 @@ var TOOLS = {
|
|
|
5543
5625
|
required: ["planId"]
|
|
5544
5626
|
}
|
|
5545
5627
|
},
|
|
5628
|
+
harmony_link_plan_task: {
|
|
5629
|
+
description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal.",
|
|
5630
|
+
inputSchema: {
|
|
5631
|
+
type: "object",
|
|
5632
|
+
properties: {
|
|
5633
|
+
planId: { type: "string", description: "Plan ID owning the criterion" },
|
|
5634
|
+
taskId: {
|
|
5635
|
+
type: "string",
|
|
5636
|
+
description: "Criterion id, from harmony_get_plan's `tasks`. Must belong to `planId`."
|
|
5637
|
+
},
|
|
5638
|
+
cardId: {
|
|
5639
|
+
type: "string",
|
|
5640
|
+
description: "Card that fulfils this criterion. Must live in the plan's own project."
|
|
5641
|
+
},
|
|
5642
|
+
status: {
|
|
5643
|
+
type: "string",
|
|
5644
|
+
enum: ["pending", "in_progress", "completed"],
|
|
5645
|
+
description: "Criterion status. Set `completed` only when the card demonstrably delivered it."
|
|
5646
|
+
}
|
|
5647
|
+
},
|
|
5648
|
+
required: ["planId", "taskId"]
|
|
5649
|
+
}
|
|
5650
|
+
},
|
|
5546
5651
|
harmony_list_playbook: {
|
|
5547
5652
|
description: "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
|
|
5548
5653
|
inputSchema: {
|
|
@@ -5637,6 +5742,19 @@ var TOOLS = {
|
|
|
5637
5742
|
required: ["playbookId"]
|
|
5638
5743
|
}
|
|
5639
5744
|
},
|
|
5745
|
+
harmony_delete_playbook: {
|
|
5746
|
+
description: "Permanently delete a playbook. IRREVERSIBLE and cascading: its version snapshots and run history are deleted with it, and every card currently running it is unbound — the card stays on the board and keeps its column, but loses its playbook, its pinned version and its stage pointer, so the agent daemon stops treating it as a stage card. Prefer harmony_update_playbook with state='deprecated' unless the playbook should never have existed: a deprecated playbook stays applicable to in-flight cards and is only hidden from new applies. Requires the playbook's creator or a workspace owner/admin — and an ARMED playbook (triggerType 'auto') takes an owner/admin even from its creator, because removing it stops the workspace's automation for everyone. Anyone else is refused and the playbook is left alone. Returns unboundCardCount (the exact number of cards it unbound) and unboundCards (up to 50 of them by id, short_id, title and the stage each one lost).",
|
|
5747
|
+
inputSchema: {
|
|
5748
|
+
type: "object",
|
|
5749
|
+
properties: {
|
|
5750
|
+
playbookId: {
|
|
5751
|
+
type: "string",
|
|
5752
|
+
description: "Playbook ID to delete (UUID)"
|
|
5753
|
+
}
|
|
5754
|
+
},
|
|
5755
|
+
required: ["playbookId"]
|
|
5756
|
+
}
|
|
5757
|
+
},
|
|
5640
5758
|
harmony_signup: {
|
|
5641
5759
|
description: "Create a new user account. Returns a JWT session for subsequent authenticated calls. No API key required.",
|
|
5642
5760
|
inputSchema: {
|
|
@@ -5931,28 +6049,58 @@ async function handleToolCall(name, args, deps) {
|
|
|
5931
6049
|
fileName: z.string().optional(),
|
|
5932
6050
|
contentType: z.string().optional()
|
|
5933
6051
|
})).parse(args.attachments) : [];
|
|
6052
|
+
const planId = args.planId ? z.string().uuid().parse(args.planId) : undefined;
|
|
6053
|
+
const planTaskId = args.planTaskId ? z.string().uuid().parse(args.planTaskId) : undefined;
|
|
6054
|
+
if (planTaskId && !planId) {
|
|
6055
|
+
throw new Error("planTaskId requires planId: no route resolves a plan from a criterion id alone. " + "Pass the plan the criterion belongs to — harmony_get_plan returns both.");
|
|
6056
|
+
}
|
|
6057
|
+
let criterion;
|
|
6058
|
+
if (planTaskId && planId) {
|
|
6059
|
+
const { tasks } = await client3.getPlan(planId);
|
|
6060
|
+
const found = findPlanTask(tasks, planTaskId);
|
|
6061
|
+
if (!found.ok)
|
|
6062
|
+
throw new Error(found.reason);
|
|
6063
|
+
criterion = found.task;
|
|
6064
|
+
}
|
|
5934
6065
|
const result = await client3.createCard(projectId, {
|
|
5935
6066
|
title,
|
|
5936
6067
|
columnId: args.columnId,
|
|
5937
6068
|
description: args.description,
|
|
5938
6069
|
priority: args.priority,
|
|
5939
6070
|
assigneeId: args.assigneeId,
|
|
5940
|
-
planId
|
|
6071
|
+
planId
|
|
5941
6072
|
});
|
|
6073
|
+
const newCardId = result.card?.id;
|
|
6074
|
+
let planTask;
|
|
6075
|
+
if (criterion && planId) {
|
|
6076
|
+
if (!newCardId) {
|
|
6077
|
+
planTask = unlinkedReport(planId, criterion, new Error("no card id was returned to link against"));
|
|
6078
|
+
} else {
|
|
6079
|
+
try {
|
|
6080
|
+
await client3.updatePlanTask(planId, criterion.id, {
|
|
6081
|
+
cardId: newCardId
|
|
6082
|
+
});
|
|
6083
|
+
planTask = linkedReport(planId, criterion, newCardId);
|
|
6084
|
+
} catch (err) {
|
|
6085
|
+
planTask = unlinkedReport(planId, criterion, err);
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
}
|
|
6089
|
+
const planTaskField = planTask ? { planTask } : {};
|
|
5942
6090
|
if (attachments.length === 0) {
|
|
5943
|
-
return { success: true, ...result };
|
|
6091
|
+
return { success: true, ...result, ...planTaskField };
|
|
5944
6092
|
}
|
|
5945
|
-
|
|
5946
|
-
if (!cardId) {
|
|
6093
|
+
if (!newCardId) {
|
|
5947
6094
|
return {
|
|
5948
6095
|
success: true,
|
|
5949
6096
|
...result,
|
|
6097
|
+
...planTaskField,
|
|
5950
6098
|
attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
|
|
5951
6099
|
};
|
|
5952
6100
|
}
|
|
5953
6101
|
const attachmentResults = await Promise.all(attachments.map(async (file) => {
|
|
5954
6102
|
try {
|
|
5955
|
-
const uploaded = await attachFileToCard(client3,
|
|
6103
|
+
const uploaded = await attachFileToCard(client3, newCardId, file);
|
|
5956
6104
|
return { ok: true, attachment: uploaded.attachment };
|
|
5957
6105
|
} catch (err) {
|
|
5958
6106
|
return {
|
|
@@ -5962,7 +6110,12 @@ async function handleToolCall(name, args, deps) {
|
|
|
5962
6110
|
};
|
|
5963
6111
|
}
|
|
5964
6112
|
}));
|
|
5965
|
-
return {
|
|
6113
|
+
return {
|
|
6114
|
+
success: true,
|
|
6115
|
+
...result,
|
|
6116
|
+
...planTaskField,
|
|
6117
|
+
attachments: attachmentResults
|
|
6118
|
+
};
|
|
5966
6119
|
}
|
|
5967
6120
|
case "harmony_update_card": {
|
|
5968
6121
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6446,15 +6599,24 @@ ${list}
|
|
|
6446
6599
|
const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
|
|
6447
6600
|
const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
|
|
6448
6601
|
const replyToId = args.replyToId !== undefined ? z.string().uuid().parse(args.replyToId) : undefined;
|
|
6449
|
-
const
|
|
6602
|
+
const sessionChoice = chooseCommentSession({
|
|
6603
|
+
cardId,
|
|
6604
|
+
tracked: getMemorySession(cardId),
|
|
6605
|
+
callerScopeId: deps.getScopeId?.(),
|
|
6606
|
+
declared: readDeclaredRunSession()
|
|
6607
|
+
});
|
|
6450
6608
|
const result = await client3.addComment(cardId, body, {
|
|
6451
6609
|
commentType,
|
|
6452
6610
|
supersedesId,
|
|
6453
6611
|
confirmsId,
|
|
6454
6612
|
replyToId,
|
|
6455
|
-
agentSessionId
|
|
6613
|
+
agentSessionId: sessionChoice.kind === "session" ? sessionChoice.agentSessionId : undefined
|
|
6456
6614
|
});
|
|
6457
|
-
return {
|
|
6615
|
+
return {
|
|
6616
|
+
success: true,
|
|
6617
|
+
...result,
|
|
6618
|
+
sessionAttribution: sessionChoice.kind === "session" ? sessionChoice.source : "none"
|
|
6619
|
+
};
|
|
6458
6620
|
}
|
|
6459
6621
|
case "harmony_get_comments": {
|
|
6460
6622
|
const cardId = z.string().uuid().parse(args.cardId);
|
|
@@ -6678,7 +6840,7 @@ ${options}
|
|
|
6678
6840
|
scopeId: deps.getScopeId?.()
|
|
6679
6841
|
});
|
|
6680
6842
|
const agentSessionId = result.session?.id;
|
|
6681
|
-
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId);
|
|
6843
|
+
initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
|
|
6682
6844
|
return {
|
|
6683
6845
|
success: true,
|
|
6684
6846
|
assignedTo,
|
|
@@ -7379,6 +7541,30 @@ ${options}
|
|
|
7379
7541
|
const result = await client3.updatePlan(planId, updates);
|
|
7380
7542
|
return { success: true, plan: result.plan };
|
|
7381
7543
|
}
|
|
7544
|
+
case "harmony_link_plan_task": {
|
|
7545
|
+
const planId = z.string().uuid().parse(args.planId);
|
|
7546
|
+
const taskId = z.string().uuid().parse(args.taskId);
|
|
7547
|
+
const cardId = args.cardId ? z.string().uuid().parse(args.cardId) : undefined;
|
|
7548
|
+
const status = args.status ? z.enum(["pending", "in_progress", "completed"]).parse(args.status) : undefined;
|
|
7549
|
+
if (!cardId && !status) {
|
|
7550
|
+
throw new Error("Nothing to do: pass cardId, status, or both.");
|
|
7551
|
+
}
|
|
7552
|
+
const { tasks } = await client3.getPlan(planId);
|
|
7553
|
+
const found = findPlanTask(tasks, taskId);
|
|
7554
|
+
if (!found.ok)
|
|
7555
|
+
throw new Error(found.reason);
|
|
7556
|
+
await client3.updatePlanTask(planId, taskId, { cardId, status });
|
|
7557
|
+
return {
|
|
7558
|
+
success: true,
|
|
7559
|
+
planTask: {
|
|
7560
|
+
planId,
|
|
7561
|
+
taskId,
|
|
7562
|
+
criterion: found.task.content ?? null,
|
|
7563
|
+
...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
|
|
7564
|
+
...status ? { status } : {}
|
|
7565
|
+
}
|
|
7566
|
+
};
|
|
7567
|
+
}
|
|
7382
7568
|
case "harmony_advance_plan": {
|
|
7383
7569
|
const planId = z.string().uuid().parse(args.planId);
|
|
7384
7570
|
const summary = args.summary;
|
|
@@ -7466,6 +7652,16 @@ ${options}
|
|
|
7466
7652
|
...warnings.length > 0 ? { warnings } : {}
|
|
7467
7653
|
};
|
|
7468
7654
|
}
|
|
7655
|
+
case "harmony_delete_playbook": {
|
|
7656
|
+
const playbookId = z.string().uuid().parse(args.playbookId);
|
|
7657
|
+
const result = await client3.deletePlaybook(playbookId);
|
|
7658
|
+
return {
|
|
7659
|
+
success: true,
|
|
7660
|
+
playbook: result.playbook,
|
|
7661
|
+
unboundCardCount: result.unboundCardCount,
|
|
7662
|
+
unboundCards: result.unboundCards
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7469
7665
|
case "harmony_save_card_as_playbook":
|
|
7470
7666
|
return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
|
|
7471
7667
|
case "harmony_signup": {
|
|
@@ -8643,6 +8839,7 @@ var SAFE_HARMONY_TOOLS = [
|
|
|
8643
8839
|
"harmony_create_plan",
|
|
8644
8840
|
"harmony_update_plan",
|
|
8645
8841
|
"harmony_advance_plan",
|
|
8842
|
+
"harmony_link_plan_task",
|
|
8646
8843
|
"harmony_remember",
|
|
8647
8844
|
"harmony_relate",
|
|
8648
8845
|
"harmony_update_memory",
|