@inetafrica/open-claudia 2.0.4 → 2.0.5
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/bin/task.js +74 -13
- package/core/config.js +3 -3
- package/core/loopback.js +23 -3
- package/core/system-prompt.js +26 -6
- package/core/tasks.js +89 -13
- package/package.json +1 -1
package/bin/task.js
CHANGED
|
@@ -1,42 +1,101 @@
|
|
|
1
1
|
// open-claudia task <subcommand>
|
|
2
2
|
// Per-channel todo list. Use this for multi-step work so progress
|
|
3
|
-
// survives compactions and restarts.
|
|
3
|
+
// survives compactions and restarts. Supports one level of hierarchy:
|
|
4
|
+
// a parent "plan" task can have child subtasks via --parent.
|
|
4
5
|
|
|
5
6
|
const { postJson } = require("./loopback-client");
|
|
6
7
|
|
|
7
8
|
const HELP = `
|
|
8
|
-
Per-channel todo list.
|
|
9
|
+
Per-channel todo list with optional plan/subtask hierarchy.
|
|
9
10
|
|
|
10
|
-
open-claudia task add "<content>"
|
|
11
|
+
open-claudia task add "<content>" [--parent <id>] [--description "<...>"]
|
|
12
|
+
open-claudia task plan "<title>" "<sub1>" "<sub2>" ... [--description "<...>"]
|
|
11
13
|
open-claudia task list [--status pending|in_progress|completed]
|
|
12
14
|
open-claudia task start <id>
|
|
13
15
|
open-claudia task done <id>
|
|
14
|
-
open-claudia task remove <id>
|
|
15
|
-
open-claudia task clear-completed
|
|
16
|
+
open-claudia task remove <id> # removing a plan removes its subtasks
|
|
17
|
+
open-claudia task clear-completed # only clears plans whose subtasks are all done
|
|
18
|
+
|
|
19
|
+
Use plans for any work with 3+ distinct steps. Mark subtasks in_progress
|
|
20
|
+
as you start them and done as you finish, so a resumed turn or a new
|
|
21
|
+
turn after compaction can pick up where you left off.
|
|
16
22
|
`;
|
|
17
23
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
function statusMark(t) {
|
|
25
|
+
return t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function takeFlag(args, name) {
|
|
29
|
+
const idx = args.indexOf(`--${name}`);
|
|
30
|
+
if (idx < 0) return null;
|
|
31
|
+
const v = args[idx + 1];
|
|
32
|
+
args.splice(idx, 2);
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderTree(list) {
|
|
37
|
+
const byParent = new Map();
|
|
38
|
+
const roots = [];
|
|
39
|
+
for (const t of list) {
|
|
40
|
+
if (t.parentId) {
|
|
41
|
+
if (!byParent.has(t.parentId)) byParent.set(t.parentId, []);
|
|
42
|
+
byParent.get(t.parentId).push(t);
|
|
43
|
+
} else {
|
|
44
|
+
roots.push(t);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const lines = [];
|
|
48
|
+
roots.forEach((root, i) => {
|
|
49
|
+
lines.push(`${i + 1}. ${statusMark(root)} ${root.content} (${root.id})`);
|
|
50
|
+
if (root.description) lines.push(` ${root.description}`);
|
|
51
|
+
(byParent.get(root.id) || []).forEach((c) => {
|
|
52
|
+
lines.push(` ${statusMark(c)} ${c.content} (${c.id})`);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
for (const orphan of list) {
|
|
56
|
+
if (orphan.parentId && !roots.find((r) => r.id === orphan.parentId)) {
|
|
57
|
+
lines.push(` ${statusMark(orphan)} ${orphan.content} (${orphan.id}) [orphan]`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines.join("\n");
|
|
21
61
|
}
|
|
22
62
|
|
|
23
63
|
async function runAdd(args) {
|
|
64
|
+
const parentId = takeFlag(args, "parent");
|
|
65
|
+
const description = takeFlag(args, "description");
|
|
24
66
|
const content = args.join(" ").trim();
|
|
25
|
-
if (!content) { console.error("Usage: task add \"<content>\""); process.exit(2); }
|
|
67
|
+
if (!content) { console.error("Usage: task add \"<content>\" [--parent <id>] [--description \"<...>\"]"); process.exit(2); }
|
|
26
68
|
try {
|
|
27
|
-
const res = await postJson("task-add", { content });
|
|
69
|
+
const res = await postJson("task-add", { content, parentId, description });
|
|
28
70
|
console.log(`Added task ${res.task.id}.`);
|
|
29
71
|
process.exit(0);
|
|
30
72
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
31
73
|
}
|
|
32
74
|
|
|
75
|
+
async function runPlan(args) {
|
|
76
|
+
const description = takeFlag(args, "description");
|
|
77
|
+
if (args.length < 2) {
|
|
78
|
+
console.error("Usage: task plan \"<title>\" \"<sub1>\" \"<sub2>\" ... [--description \"<...>\"]");
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
const [title, ...subtasks] = args;
|
|
82
|
+
try {
|
|
83
|
+
const res = await postJson("task-plan", { title, subtasks, description });
|
|
84
|
+
const parent = res.plan.parent;
|
|
85
|
+
const kids = res.plan.children || [];
|
|
86
|
+
console.log(`Plan ${parent.id}: ${parent.content}`);
|
|
87
|
+
kids.forEach((c) => console.log(` - ${c.content} (${c.id})`));
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
async function runList(args) {
|
|
34
|
-
const status = args
|
|
93
|
+
const status = args.includes("--status") ? args[args.indexOf("--status") + 1] : null;
|
|
35
94
|
try {
|
|
36
95
|
const res = await postJson("task-list", status ? { status } : {});
|
|
37
96
|
const list = res.tasks || [];
|
|
38
97
|
if (list.length === 0) { console.log("No tasks."); process.exit(0); }
|
|
39
|
-
|
|
98
|
+
console.log(renderTree(list));
|
|
40
99
|
process.exit(0);
|
|
41
100
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
42
101
|
}
|
|
@@ -58,7 +117,8 @@ async function runRemove(args) {
|
|
|
58
117
|
try {
|
|
59
118
|
const res = await postJson("task-remove", { id });
|
|
60
119
|
if (!res.ok) { console.error(`Not found: ${id}`); process.exit(1); }
|
|
61
|
-
|
|
120
|
+
const extra = (res.removed && res.removed.alsoRemoved) ? ` (and ${res.removed.alsoRemoved} subtask${res.removed.alsoRemoved === 1 ? "" : "s"})` : "";
|
|
121
|
+
console.log(`Removed ${id}${extra}.`);
|
|
62
122
|
process.exit(0);
|
|
63
123
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
64
124
|
}
|
|
@@ -77,6 +137,7 @@ async function run(args) {
|
|
|
77
137
|
if (!sub || sub === "--help" || sub === "help") { console.log(HELP); process.exit(sub ? 0 : 2); }
|
|
78
138
|
switch (sub) {
|
|
79
139
|
case "add": return runAdd(rest);
|
|
140
|
+
case "plan": return runPlan(rest);
|
|
80
141
|
case "list": case "ls": return runList(rest);
|
|
81
142
|
case "start": return runUpdate(rest, "in_progress");
|
|
82
143
|
case "done": case "complete": return runUpdate(rest, "completed");
|
package/core/config.js
CHANGED
|
@@ -78,7 +78,7 @@ if (!CLAUDE_PATH) { console.error("CLAUDE_PATH not set"); process.exit(1); }
|
|
|
78
78
|
if (!fs.existsSync(WORKSPACE)) {
|
|
79
79
|
try {
|
|
80
80
|
fs.mkdirSync(WORKSPACE, { recursive: true });
|
|
81
|
-
console.
|
|
81
|
+
console.error(`Created workspace: ${WORKSPACE}`);
|
|
82
82
|
} catch (e) {
|
|
83
83
|
console.error(`Failed to create workspace: ${e.message}`);
|
|
84
84
|
process.exit(1);
|
|
@@ -99,14 +99,14 @@ if (!resolvedCursorPath) {
|
|
|
99
99
|
try { resolvedCursorPath = execSync("which agent 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
100
100
|
catch (e) { resolvedCursorPath = null; }
|
|
101
101
|
}
|
|
102
|
-
if (resolvedCursorPath) console.
|
|
102
|
+
if (resolvedCursorPath) console.error(`Cursor Agent CLI: ${resolvedCursorPath}`);
|
|
103
103
|
|
|
104
104
|
let resolvedCodexPath = CODEX_PATH;
|
|
105
105
|
if (!resolvedCodexPath) {
|
|
106
106
|
try { resolvedCodexPath = execSync("which codex 2>/dev/null", { encoding: "utf-8" }).trim() || null; }
|
|
107
107
|
catch (e) { resolvedCodexPath = null; }
|
|
108
108
|
}
|
|
109
|
-
if (resolvedCodexPath) console.
|
|
109
|
+
if (resolvedCodexPath) console.error(`Codex CLI: ${resolvedCodexPath}`);
|
|
110
110
|
|
|
111
111
|
if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
112
112
|
if (!fs.existsSync(FILES_DIR)) fs.mkdirSync(FILES_DIR, { recursive: true });
|
package/core/loopback.js
CHANGED
|
@@ -64,7 +64,7 @@ function readBodyToFile(req, dest) {
|
|
|
64
64
|
const SEND_KINDS = new Set(["send-file", "send-voice", "send-photo"]);
|
|
65
65
|
const JSON_KINDS = new Set([
|
|
66
66
|
"schedule-wakeup", "cron-add", "cron-remove", "job-list",
|
|
67
|
-
"task-add", "task-update", "task-remove", "task-list", "task-clear-completed",
|
|
67
|
+
"task-add", "task-plan", "task-update", "task-remove", "task-list", "task-tree", "task-clear-completed",
|
|
68
68
|
]);
|
|
69
69
|
|
|
70
70
|
function readBodyAsString(req, max = 64 * 1024) {
|
|
@@ -177,8 +177,28 @@ async function handleJson(req, res, url, kind) {
|
|
|
177
177
|
|
|
178
178
|
if (kind === "task-add") {
|
|
179
179
|
if (!payload.content) return reply(res, 400, { error: "missing content" });
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
try {
|
|
181
|
+
const t = tasksStore.add(adapterId, channelId, payload.content, {
|
|
182
|
+
parentId: payload.parentId || null,
|
|
183
|
+
description: payload.description || null,
|
|
184
|
+
});
|
|
185
|
+
return reply(res, 200, { ok: true, task: t });
|
|
186
|
+
} catch (e) { return reply(res, 400, { error: e.message }); }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (kind === "task-plan") {
|
|
190
|
+
if (!payload.title) return reply(res, 400, { error: "missing title" });
|
|
191
|
+
const subs = Array.isArray(payload.subtasks) ? payload.subtasks : [];
|
|
192
|
+
if (subs.length === 0) return reply(res, 400, { error: "subtasks must be a non-empty array" });
|
|
193
|
+
const result = tasksStore.plan(adapterId, channelId, payload.title, subs, {
|
|
194
|
+
description: payload.description || null,
|
|
195
|
+
});
|
|
196
|
+
return reply(res, 200, { ok: true, plan: result });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (kind === "task-tree") {
|
|
200
|
+
const t = tasksStore.tree(adapterId, channelId);
|
|
201
|
+
return reply(res, 200, { ok: true, tree: t });
|
|
182
202
|
}
|
|
183
203
|
|
|
184
204
|
if (kind === "task-update") {
|
package/core/system-prompt.js
CHANGED
|
@@ -6,9 +6,10 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { SOUL_FILE, CRONS_FILE, VAULT_FILE, FILES_DIR, BOT_DIR, WHISPER_CLI, FFMPEG } = require("./config");
|
|
8
8
|
const { currentState } = require("./state");
|
|
9
|
-
const { currentAdapter } = require("./context");
|
|
9
|
+
const { currentAdapter, currentChannelId } = require("./context");
|
|
10
10
|
const { vault } = require("./vault-store");
|
|
11
11
|
const { transcriptPointerNote } = require("./transcripts");
|
|
12
|
+
const tasksStore = require("./tasks");
|
|
12
13
|
|
|
13
14
|
function loadSoul() {
|
|
14
15
|
try { return fs.readFileSync(SOUL_FILE, "utf-8"); }
|
|
@@ -20,10 +21,22 @@ function buildSystemPrompt() {
|
|
|
20
21
|
const soul = loadSoul();
|
|
21
22
|
const hasVoice = WHISPER_CLI && FFMPEG;
|
|
22
23
|
const adapter = currentAdapter();
|
|
24
|
+
const channelId = currentChannelId();
|
|
23
25
|
const channelLabel = adapter
|
|
24
26
|
? (adapter.type === "kazee" ? "Kazee Chat" : adapter.type === "telegram" ? "Telegram" : adapter.type)
|
|
25
27
|
: "Telegram";
|
|
26
28
|
|
|
29
|
+
let pendingTasksBlock = "";
|
|
30
|
+
if (adapter && channelId) {
|
|
31
|
+
try {
|
|
32
|
+
const pending = tasksStore.pendingSummary(adapter.id, channelId);
|
|
33
|
+
if (pending.length > 0) {
|
|
34
|
+
const tree = tasksStore.formatTree(adapter.id, channelId, { showIds: true });
|
|
35
|
+
pendingTasksBlock = `\n## Pending tasks\nYou may be resuming prior work. Before starting anything new, review these and decide whether to continue, update, or abandon them. Mark a subtask in_progress when you actually start it and completed when it's done.\n\n${tree}\n`;
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {}
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
return `
|
|
28
41
|
${soul}
|
|
29
42
|
|
|
@@ -43,7 +56,7 @@ ${soul}
|
|
|
43
56
|
- Received user files directory: ${FILES_DIR}
|
|
44
57
|
|
|
45
58
|
${transcriptPointerNote(state)}
|
|
46
|
-
|
|
59
|
+
${pendingTasksBlock}
|
|
47
60
|
## Delivery
|
|
48
61
|
Reply normally in your final answer. To send a file, image, or voice clip back to the current chat, run the bot CLI from inside this task — channel context is already in the env:
|
|
49
62
|
- \`open-claudia send-file <path> [caption]\` — any document/binary
|
|
@@ -60,10 +73,17 @@ Wake-ups and crons (real schedulers, not hallucinations — use them instead of
|
|
|
60
73
|
- \`open-claudia cron-list\` — list all wakeups + crons on this channel.
|
|
61
74
|
- \`open-claudia cron-remove <id>\` — cancel one.
|
|
62
75
|
|
|
63
|
-
Persistent todo list (per channel;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
76
|
+
Persistent todo list with plans + subtasks (per channel; survives compaction and restart).
|
|
77
|
+
|
|
78
|
+
For any work with 3+ distinct steps you MUST create a plan before starting. The plan is a parent task whose children are the steps. As you work, mark each subtask in_progress when you begin and completed when done — this is how a resumed turn (after compaction, restart, or wakeup) sees where you left off. If pending tasks already exist when you start a turn they will be shown under "## Pending tasks" above; check them first.
|
|
79
|
+
|
|
80
|
+
- \`open-claudia task plan "<plan title>" "<step 1>" "<step 2>" "<step 3>" [--description "<why / success criteria>"]\` — atomic create
|
|
81
|
+
- \`open-claudia task add "<content>" [--parent <plan-id>] [--description "..."]\` — add a single task or subtask
|
|
82
|
+
- \`open-claudia task list\` — renders as a tree
|
|
83
|
+
- \`open-claudia task start <id>\` / \`task done <id>\` / \`task remove <id>\` (removing a plan removes its subtasks)
|
|
84
|
+
- \`open-claudia task clear-completed\` — only clears plans whose subtasks are all done
|
|
85
|
+
|
|
86
|
+
For one-off work under 3 steps, skip the plan and just track it in your own response.
|
|
67
87
|
|
|
68
88
|
Sub-agents (spawn a fresh throwaway Claude for focused research — output comes back on stdout):
|
|
69
89
|
- \`open-claudia agent "<prompt>" [--role "<role>"]\`
|
package/core/tasks.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// multi-step work that should survive a turn, a compaction, or a
|
|
3
3
|
// restart. Scoped by adapter+channel so Telegram and Kazee see
|
|
4
4
|
// separate lists.
|
|
5
|
+
//
|
|
6
|
+
// Tasks support a single level of hierarchy via parentId: a top-level
|
|
7
|
+
// "plan" task can have child subtasks. This lets the agent break a big
|
|
8
|
+
// piece of work into a plan whose progress is visible at a glance.
|
|
5
9
|
|
|
6
10
|
const fs = require("fs");
|
|
7
11
|
const path = require("path");
|
|
@@ -34,12 +38,20 @@ function nextId() {
|
|
|
34
38
|
return `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
function add(adapter, channelId, content) {
|
|
41
|
+
function add(adapter, channelId, content, opts = {}) {
|
|
38
42
|
const list = load(adapter, channelId);
|
|
43
|
+
const parentId = opts.parentId || null;
|
|
44
|
+
if (parentId) {
|
|
45
|
+
const parent = list.find((t) => t.id === parentId);
|
|
46
|
+
if (!parent) throw new Error(`parent task not found: ${parentId}`);
|
|
47
|
+
if (parent.parentId) throw new Error(`nested subtasks not supported: ${parentId} is already a subtask`);
|
|
48
|
+
}
|
|
39
49
|
const t = {
|
|
40
50
|
id: nextId(),
|
|
41
51
|
content: String(content || "").trim(),
|
|
42
52
|
status: "pending",
|
|
53
|
+
parentId,
|
|
54
|
+
description: opts.description ? String(opts.description) : null,
|
|
43
55
|
createdAt: Date.now(),
|
|
44
56
|
updatedAt: Date.now(),
|
|
45
57
|
};
|
|
@@ -48,6 +60,16 @@ function add(adapter, channelId, content) {
|
|
|
48
60
|
return t;
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
function plan(adapter, channelId, title, subtaskContents, opts = {}) {
|
|
64
|
+
const parent = add(adapter, channelId, title, { description: opts.description || null });
|
|
65
|
+
const children = [];
|
|
66
|
+
for (const c of subtaskContents || []) {
|
|
67
|
+
if (!String(c || "").trim()) continue;
|
|
68
|
+
children.push(add(adapter, channelId, c, { parentId: parent.id }));
|
|
69
|
+
}
|
|
70
|
+
return { parent, children };
|
|
71
|
+
}
|
|
72
|
+
|
|
51
73
|
function update(adapter, channelId, id, patch) {
|
|
52
74
|
const list = load(adapter, channelId);
|
|
53
75
|
const idx = list.findIndex((t) => t.id === id);
|
|
@@ -59,17 +81,32 @@ function update(adapter, channelId, id, patch) {
|
|
|
59
81
|
|
|
60
82
|
function remove(adapter, channelId, id) {
|
|
61
83
|
const list = load(adapter, channelId);
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
const target = list.find((t) => t.id === id);
|
|
85
|
+
if (!target) return null;
|
|
86
|
+
const ids = new Set([id]);
|
|
87
|
+
if (!target.parentId) {
|
|
88
|
+
for (const t of list) if (t.parentId === id) ids.add(t.id);
|
|
89
|
+
}
|
|
90
|
+
const next = list.filter((t) => !ids.has(t.id));
|
|
91
|
+
save(adapter, channelId, next);
|
|
92
|
+
return { removed: target, alsoRemoved: ids.size - 1 };
|
|
67
93
|
}
|
|
68
94
|
|
|
69
95
|
function clearCompleted(adapter, channelId) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
const all = load(adapter, channelId);
|
|
97
|
+
const drop = new Set();
|
|
98
|
+
for (const t of all) {
|
|
99
|
+
if (t.status !== "completed") continue;
|
|
100
|
+
if (t.parentId) { drop.add(t.id); continue; }
|
|
101
|
+
const kids = all.filter((c) => c.parentId === t.id);
|
|
102
|
+
if (kids.length === 0 || kids.every((c) => c.status === "completed")) {
|
|
103
|
+
drop.add(t.id);
|
|
104
|
+
for (const c of kids) drop.add(c.id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const remaining = all.filter((t) => !drop.has(t.id));
|
|
108
|
+
save(adapter, channelId, remaining);
|
|
109
|
+
return remaining;
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
function list(adapter, channelId, opts = {}) {
|
|
@@ -78,15 +115,54 @@ function list(adapter, channelId, opts = {}) {
|
|
|
78
115
|
return all;
|
|
79
116
|
}
|
|
80
117
|
|
|
118
|
+
function tree(adapter, channelId) {
|
|
119
|
+
const all = load(adapter, channelId);
|
|
120
|
+
const byParent = new Map();
|
|
121
|
+
const roots = [];
|
|
122
|
+
for (const t of all) {
|
|
123
|
+
if (t.parentId) {
|
|
124
|
+
if (!byParent.has(t.parentId)) byParent.set(t.parentId, []);
|
|
125
|
+
byParent.get(t.parentId).push(t);
|
|
126
|
+
} else {
|
|
127
|
+
roots.push(t);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return roots.map((r) => ({ ...r, children: byParent.get(r.id) || [] }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pendingSummary(adapter, channelId) {
|
|
134
|
+
const all = load(adapter, channelId);
|
|
135
|
+
return all.filter((t) => t.status === "pending" || t.status === "in_progress");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function statusMark(t) {
|
|
139
|
+
return t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
140
|
+
}
|
|
141
|
+
|
|
81
142
|
function format(t, idx) {
|
|
82
|
-
const mark = t.status === "completed" ? "[x]" : t.status === "in_progress" ? "[~]" : "[ ]";
|
|
83
143
|
const num = typeof idx === "number" ? `${idx + 1}. ` : "";
|
|
84
|
-
return `${num}${
|
|
144
|
+
return `${num}${statusMark(t)} ${t.content}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatTree(adapter, channelId, opts = {}) {
|
|
148
|
+
const showIds = opts.showIds !== false;
|
|
149
|
+
const t = tree(adapter, channelId);
|
|
150
|
+
const lines = [];
|
|
151
|
+
t.forEach((root, i) => {
|
|
152
|
+
const head = `${i + 1}. ${statusMark(root)} ${root.content}` + (showIds ? ` (${root.id})` : "");
|
|
153
|
+
lines.push(head);
|
|
154
|
+
if (root.description) lines.push(` ${root.description}`);
|
|
155
|
+
root.children.forEach((child) => {
|
|
156
|
+
lines.push(` ${statusMark(child)} ${child.content}` + (showIds ? ` (${child.id})` : ""));
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
return lines.join("\n");
|
|
85
160
|
}
|
|
86
161
|
|
|
87
162
|
module.exports = {
|
|
88
163
|
filePathFor,
|
|
89
164
|
load, save,
|
|
90
|
-
add, update, remove, list, clearCompleted,
|
|
91
|
-
|
|
165
|
+
add, plan, update, remove, list, tree, clearCompleted,
|
|
166
|
+
pendingSummary,
|
|
167
|
+
format, formatTree, statusMark,
|
|
92
168
|
};
|
package/package.json
CHANGED