heartbeat-opencode-plugin 0.1.0 → 0.1.2
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/index.ts +3 -1
- package/memory/index.ts +13 -9
- package/package.json +1 -1
- package/scheduler/index.ts +120 -15
- package/task-manager/index.ts +79 -28
package/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { SchedulerPlugin } from "./scheduler";
|
|
|
4
4
|
import { TaskManagerPlugin, getTask, listTasks } from "./task-manager";
|
|
5
5
|
|
|
6
6
|
type PluginOutput = Awaited<ReturnType<Plugin>>;
|
|
7
|
+
const OUTPUT_TAG_NOTE =
|
|
8
|
+
"Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
|
|
7
9
|
|
|
8
10
|
function mergeTools(outputs: PluginOutput[]): Record<string, unknown> {
|
|
9
11
|
const toolMap: Record<string, unknown> = {};
|
|
@@ -30,7 +32,7 @@ export const HeartbeatPlugin: Plugin = async (ctx) => {
|
|
|
30
32
|
|
|
31
33
|
heartbeat_boot_context: tool({
|
|
32
34
|
description:
|
|
33
|
-
|
|
35
|
+
`Boot helper for a CPU cycle. Returns task state + prior memory for [program][taskId]. ${OUTPUT_TAG_NOTE}`,
|
|
34
36
|
args: {
|
|
35
37
|
program: tool.schema.string().describe("Program name"),
|
|
36
38
|
taskId: tool.schema.string().describe("Task identifier"),
|
package/memory/index.ts
CHANGED
|
@@ -35,6 +35,8 @@ export function writeLog(input: {
|
|
|
35
35
|
|
|
36
36
|
const LOG_ENTRY_PREFIX =
|
|
37
37
|
/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\]\s/;
|
|
38
|
+
const OUTPUT_TAG_NOTE =
|
|
39
|
+
"Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
|
|
38
40
|
|
|
39
41
|
function readAllEntries(limit?: number): string[] {
|
|
40
42
|
const path = getLogFilePath();
|
|
@@ -180,7 +182,8 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
180
182
|
return {
|
|
181
183
|
tool: {
|
|
182
184
|
memory_write: tool({
|
|
183
|
-
description:
|
|
185
|
+
description:
|
|
186
|
+
`Write a tagged log line as [program][taskId] message. ${OUTPUT_TAG_NOTE}`,
|
|
184
187
|
args: {
|
|
185
188
|
program: tool.schema.string().describe("Program name"),
|
|
186
189
|
taskId: tool.schema.string().describe("Task identifier"),
|
|
@@ -193,7 +196,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
193
196
|
|
|
194
197
|
write_log: tool({
|
|
195
198
|
description:
|
|
196
|
-
|
|
199
|
+
`Alias for memory_write. Writes tagged memory lines into the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
|
|
197
200
|
args: {
|
|
198
201
|
program: tool.schema.string().describe("Program name"),
|
|
199
202
|
taskId: tool.schema.string().describe("Task identifier"),
|
|
@@ -206,7 +209,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
206
209
|
|
|
207
210
|
search_memory: tool({
|
|
208
211
|
description:
|
|
209
|
-
|
|
212
|
+
`Search memory by [program] and optional [taskId]. Use this on boot to restore previous cycle context. ${OUTPUT_TAG_NOTE}`,
|
|
210
213
|
args: {
|
|
211
214
|
program: tool.schema.string().describe("Program name"),
|
|
212
215
|
taskId: tool.schema
|
|
@@ -227,7 +230,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
227
230
|
}),
|
|
228
231
|
|
|
229
232
|
read_program: tool({
|
|
230
|
-
description:
|
|
233
|
+
description: `Read all log lines containing [program]. ${OUTPUT_TAG_NOTE}`,
|
|
231
234
|
args: {
|
|
232
235
|
program: tool.schema.string().describe("Program name"),
|
|
233
236
|
limit: tool.schema.number().optional().describe("Optional max lines"),
|
|
@@ -238,7 +241,8 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
238
241
|
}),
|
|
239
242
|
|
|
240
243
|
read_program_task: tool({
|
|
241
|
-
description:
|
|
244
|
+
description:
|
|
245
|
+
`Read all log lines containing [program][taskId]. ${OUTPUT_TAG_NOTE}`,
|
|
242
246
|
args: {
|
|
243
247
|
program: tool.schema.string().describe("Program name"),
|
|
244
248
|
taskId: tool.schema.string().describe("Task identifier"),
|
|
@@ -251,7 +255,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
251
255
|
|
|
252
256
|
memory_grep: tool({
|
|
253
257
|
description:
|
|
254
|
-
|
|
258
|
+
`Regex grep across the shared heartbeat log file. Useful for free-form memory queries. ${OUTPUT_TAG_NOTE}`,
|
|
255
259
|
args: {
|
|
256
260
|
pattern: tool.schema.string().describe("JavaScript regex pattern"),
|
|
257
261
|
limit: tool.schema.number().optional().describe("Optional max lines"),
|
|
@@ -266,7 +270,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
266
270
|
}),
|
|
267
271
|
|
|
268
272
|
grep_logs: tool({
|
|
269
|
-
description:
|
|
273
|
+
description: `Alias for memory_grep. ${OUTPUT_TAG_NOTE}`,
|
|
270
274
|
args: {
|
|
271
275
|
pattern: tool.schema.string().describe("JavaScript regex pattern"),
|
|
272
276
|
limit: tool.schema.number().optional().describe("Optional max lines"),
|
|
@@ -281,7 +285,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
281
285
|
}),
|
|
282
286
|
|
|
283
287
|
tail_logs: tool({
|
|
284
|
-
description:
|
|
288
|
+
description: `Return recent lines from the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
|
|
285
289
|
args: {
|
|
286
290
|
lines: tool.schema
|
|
287
291
|
.number()
|
|
@@ -294,7 +298,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
|
|
|
294
298
|
}),
|
|
295
299
|
|
|
296
300
|
log_info: tool({
|
|
297
|
-
description:
|
|
301
|
+
description: `Get shared log file path and stats. ${OUTPUT_TAG_NOTE}`,
|
|
298
302
|
args: {},
|
|
299
303
|
async execute() {
|
|
300
304
|
return JSON.stringify(getLogInfo(), null, 2);
|
package/package.json
CHANGED
package/scheduler/index.ts
CHANGED
|
@@ -70,6 +70,24 @@ export interface RunResult {
|
|
|
70
70
|
const IS_MAC = process.platform === "darwin";
|
|
71
71
|
const LAUNCH_AGENTS_DIR = path.join(homedir(), "Library", "LaunchAgents");
|
|
72
72
|
const SCHEDULER_ENTRY = fileURLToPath(import.meta.url);
|
|
73
|
+
const OPENCODE_COMMAND = "opencode";
|
|
74
|
+
const OPENCODE_FALLBACK_PATHS = [
|
|
75
|
+
"/opt/homebrew/bin/opencode",
|
|
76
|
+
"/usr/local/bin/opencode",
|
|
77
|
+
];
|
|
78
|
+
const DEFAULT_EXEC_PATH_ENTRIES = [
|
|
79
|
+
"/opt/homebrew/bin",
|
|
80
|
+
"/opt/homebrew/sbin",
|
|
81
|
+
"/usr/local/bin",
|
|
82
|
+
"/usr/local/sbin",
|
|
83
|
+
"/usr/bin",
|
|
84
|
+
"/bin",
|
|
85
|
+
"/usr/sbin",
|
|
86
|
+
"/sbin",
|
|
87
|
+
"/Library/Apple/usr/bin",
|
|
88
|
+
];
|
|
89
|
+
const OUTPUT_TAG_NOTE =
|
|
90
|
+
"Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
|
|
73
91
|
|
|
74
92
|
function ensureDir(dir: string): void {
|
|
75
93
|
if (!fs.existsSync(dir)) {
|
|
@@ -103,6 +121,75 @@ function appendJobLog(job: Pick<Job, "id">, message: string): void {
|
|
|
103
121
|
appendLog(`[Scheduler][job:${job.id}] ${message}`);
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
function isExecutable(filePath: string): boolean {
|
|
125
|
+
try {
|
|
126
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findExecutableInPath(command: string, envPath?: string): string | null {
|
|
134
|
+
if (!envPath) return null;
|
|
135
|
+
|
|
136
|
+
const pathEntries = envPath.split(path.delimiter).filter(Boolean);
|
|
137
|
+
for (const pathEntry of pathEntries) {
|
|
138
|
+
const candidate = path.join(pathEntry, command);
|
|
139
|
+
if (isExecutable(candidate)) {
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveOpencodeCommand(command: string): string {
|
|
148
|
+
if (path.basename(command) !== OPENCODE_COMMAND) {
|
|
149
|
+
return command;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isExecutable(command)) {
|
|
153
|
+
return command;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const envOverride = process.env.OPENCODE_BIN?.trim();
|
|
157
|
+
if (envOverride && isExecutable(envOverride)) {
|
|
158
|
+
return envOverride;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fromPath = findExecutableInPath(OPENCODE_COMMAND, process.env.PATH);
|
|
162
|
+
if (fromPath) {
|
|
163
|
+
return fromPath;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const fallbackPath of OPENCODE_FALLBACK_PATHS) {
|
|
167
|
+
if (isExecutable(fallbackPath)) {
|
|
168
|
+
return fallbackPath;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return command;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildExecutablePath(): string {
|
|
176
|
+
const currentEntries = (process.env.PATH ?? "")
|
|
177
|
+
.split(path.delimiter)
|
|
178
|
+
.map((entry) => entry.trim())
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
|
|
181
|
+
const merged = [...currentEntries, ...DEFAULT_EXEC_PATH_ENTRIES];
|
|
182
|
+
const unique = Array.from(new Set(merged));
|
|
183
|
+
return unique.join(path.delimiter);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildJobEnv(): NodeJS.ProcessEnv {
|
|
187
|
+
return {
|
|
188
|
+
...process.env,
|
|
189
|
+
PATH: buildExecutablePath(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
106
193
|
function readJob(jobId: string): Job | null {
|
|
107
194
|
const file = jobFilePath(jobId);
|
|
108
195
|
if (!fs.existsSync(file)) return null;
|
|
@@ -228,7 +315,7 @@ export function createOpencodeJob(input: CreateOpencodeJobInput): Job {
|
|
|
228
315
|
kind: "opencode",
|
|
229
316
|
schedule: input.schedule,
|
|
230
317
|
prompt: input.prompt,
|
|
231
|
-
command:
|
|
318
|
+
command: resolveOpencodeCommand(OPENCODE_COMMAND),
|
|
232
319
|
args: ["run", heartbeatPrompt],
|
|
233
320
|
workdir: input.workdir ?? getDefaultWorkdir(),
|
|
234
321
|
enabled: input.enabled ?? true,
|
|
@@ -261,7 +348,7 @@ export function updateJob(
|
|
|
261
348
|
if (!nextPrompt) {
|
|
262
349
|
throw new Error(`Opencode job ${existing.id} is missing prompt content.`);
|
|
263
350
|
}
|
|
264
|
-
nextCommand =
|
|
351
|
+
nextCommand = resolveOpencodeCommand(OPENCODE_COMMAND);
|
|
265
352
|
nextArgs = [
|
|
266
353
|
"run",
|
|
267
354
|
buildHeartbeatPrompt({
|
|
@@ -364,14 +451,23 @@ export function runJob(jobId: string): Promise<RunResult> {
|
|
|
364
451
|
writeJob(job);
|
|
365
452
|
|
|
366
453
|
appendJobLog(job, `Starting job "${job.name}"`);
|
|
367
|
-
|
|
454
|
+
const jobEnv = buildJobEnv();
|
|
455
|
+
const resolvedCommand = job.kind === "opencode"
|
|
456
|
+
? resolveOpencodeCommand(job.command)
|
|
457
|
+
: job.command;
|
|
458
|
+
if (resolvedCommand !== job.command) {
|
|
459
|
+
job.command = resolvedCommand;
|
|
460
|
+
appendJobLog(job, `Resolved opencode executable: ${resolvedCommand}`);
|
|
461
|
+
}
|
|
462
|
+
appendJobLog(job, `Command: ${resolvedCommand} ${job.args.join(" ")}`);
|
|
368
463
|
appendJobLog(job, `Workdir: ${job.workdir}`);
|
|
464
|
+
appendJobLog(job, `PATH: ${jobEnv.PATH ?? ""}`);
|
|
369
465
|
|
|
370
466
|
let child: ReturnType<typeof spawn>;
|
|
371
467
|
try {
|
|
372
|
-
child = spawn(
|
|
468
|
+
child = spawn(resolvedCommand, job.args, {
|
|
373
469
|
cwd: job.workdir,
|
|
374
|
-
env:
|
|
470
|
+
env: jobEnv,
|
|
375
471
|
shell: false,
|
|
376
472
|
stdio: ["ignore", "pipe", "pipe"],
|
|
377
473
|
});
|
|
@@ -625,6 +721,7 @@ function buildCalendarXml(calendars: CalendarEntry[]): string {
|
|
|
625
721
|
function createLaunchdPlist(job: Job): string {
|
|
626
722
|
const label = getConfigLaunchdLabel(job.id);
|
|
627
723
|
const parsed = parseCronToLaunchd(job.schedule);
|
|
724
|
+
const executablePath = buildExecutablePath();
|
|
628
725
|
|
|
629
726
|
const scheduleBlock = parsed.startInterval
|
|
630
727
|
? ` <key>StartInterval</key>\n <integer>${parsed.startInterval}</integer>`
|
|
@@ -646,6 +743,11 @@ function createLaunchdPlist(job: Job): string {
|
|
|
646
743
|
<string>run</string>
|
|
647
744
|
<string>${escapeXml(job.id)}</string>
|
|
648
745
|
</array>
|
|
746
|
+
<key>EnvironmentVariables</key>
|
|
747
|
+
<dict>
|
|
748
|
+
<key>PATH</key>
|
|
749
|
+
<string>${escapeXml(executablePath)}</string>
|
|
750
|
+
</dict>
|
|
649
751
|
${scheduleBlock}
|
|
650
752
|
<key>StandardOutPath</key>
|
|
651
753
|
<string>${escapeXml(sharedLogPath())}</string>
|
|
@@ -752,7 +854,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
752
854
|
tool: {
|
|
753
855
|
scheduler_create_opencode_job: tool({
|
|
754
856
|
description:
|
|
755
|
-
|
|
857
|
+
`Create a human-managed cron job that runs \`opencode run\` with the provided prompt. ${OUTPUT_TAG_NOTE}`,
|
|
756
858
|
args: {
|
|
757
859
|
name: tool.schema.string().describe("Job name"),
|
|
758
860
|
schedule: tool.schema.string().describe("Cron schedule (e.g. */5 * * * *)"),
|
|
@@ -803,7 +905,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
803
905
|
|
|
804
906
|
scheduler_create_command_job: tool({
|
|
805
907
|
description:
|
|
806
|
-
|
|
908
|
+
`Create a human-managed cron job for a generic command. Output streams to the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
|
|
807
909
|
args: {
|
|
808
910
|
name: tool.schema.string().describe("Job name"),
|
|
809
911
|
schedule: tool.schema.string().describe("Cron schedule"),
|
|
@@ -830,7 +932,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
830
932
|
}),
|
|
831
933
|
|
|
832
934
|
scheduler_list_jobs: tool({
|
|
833
|
-
description:
|
|
935
|
+
description: `List all heartbeat scheduler jobs. ${OUTPUT_TAG_NOTE}`,
|
|
834
936
|
args: {},
|
|
835
937
|
async execute() {
|
|
836
938
|
const jobs = listJobs();
|
|
@@ -842,7 +944,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
842
944
|
}),
|
|
843
945
|
|
|
844
946
|
scheduler_get_job: tool({
|
|
845
|
-
description:
|
|
947
|
+
description: `Get a scheduler job by id. ${OUTPUT_TAG_NOTE}`,
|
|
846
948
|
args: {
|
|
847
949
|
jobId: tool.schema.string().describe("Job ID"),
|
|
848
950
|
},
|
|
@@ -856,7 +958,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
856
958
|
}),
|
|
857
959
|
|
|
858
960
|
scheduler_update_job: tool({
|
|
859
|
-
description:
|
|
961
|
+
description:
|
|
962
|
+
`Update schedule, prompt, or metadata of an existing job. ${OUTPUT_TAG_NOTE}`,
|
|
860
963
|
args: {
|
|
861
964
|
jobId: tool.schema.string().describe("Job ID"),
|
|
862
965
|
name: tool.schema.string().optional().describe("Updated name"),
|
|
@@ -883,7 +986,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
883
986
|
}),
|
|
884
987
|
|
|
885
988
|
scheduler_run_job: tool({
|
|
886
|
-
description:
|
|
989
|
+
description:
|
|
990
|
+
`Run a job immediately. Output is appended to one shared log file. ${OUTPUT_TAG_NOTE}`,
|
|
887
991
|
args: {
|
|
888
992
|
jobId: tool.schema.string().describe("Job ID"),
|
|
889
993
|
},
|
|
@@ -898,7 +1002,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
898
1002
|
}),
|
|
899
1003
|
|
|
900
1004
|
scheduler_install_job: tool({
|
|
901
|
-
description:
|
|
1005
|
+
description: `Install a job in macOS launchd. ${OUTPUT_TAG_NOTE}`,
|
|
902
1006
|
args: {
|
|
903
1007
|
jobId: tool.schema.string().describe("Job ID"),
|
|
904
1008
|
},
|
|
@@ -916,7 +1020,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
916
1020
|
}),
|
|
917
1021
|
|
|
918
1022
|
scheduler_uninstall_job: tool({
|
|
919
|
-
description:
|
|
1023
|
+
description: `Uninstall a job from macOS launchd. ${OUTPUT_TAG_NOTE}`,
|
|
920
1024
|
args: {
|
|
921
1025
|
jobId: tool.schema.string().describe("Job ID"),
|
|
922
1026
|
},
|
|
@@ -934,7 +1038,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
934
1038
|
}),
|
|
935
1039
|
|
|
936
1040
|
scheduler_delete_job: tool({
|
|
937
|
-
description:
|
|
1041
|
+
description:
|
|
1042
|
+
`Delete a scheduler job and uninstall its launchd entry if present. ${OUTPUT_TAG_NOTE}`,
|
|
938
1043
|
args: {
|
|
939
1044
|
jobId: tool.schema.string().describe("Job ID"),
|
|
940
1045
|
},
|
|
@@ -945,7 +1050,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
|
|
|
945
1050
|
}),
|
|
946
1051
|
|
|
947
1052
|
scheduler_info: tool({
|
|
948
|
-
description:
|
|
1053
|
+
description: `Show scheduler storage paths and shared log path. ${OUTPUT_TAG_NOTE}`,
|
|
949
1054
|
args: {},
|
|
950
1055
|
async execute() {
|
|
951
1056
|
return JSON.stringify(
|
package/task-manager/index.ts
CHANGED
|
@@ -40,6 +40,8 @@ const VALID_PRIORITIES = new Set<TaskPriority>([
|
|
|
40
40
|
"high",
|
|
41
41
|
"critical",
|
|
42
42
|
]);
|
|
43
|
+
const OUTPUT_TAG_NOTE =
|
|
44
|
+
"Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
|
|
43
45
|
|
|
44
46
|
function nowIso(): string {
|
|
45
47
|
return new Date().toISOString();
|
|
@@ -128,7 +130,8 @@ export function listTasks(filters?: TaskFilters): Task[] {
|
|
|
128
130
|
const tasks = storeInstance().read();
|
|
129
131
|
return tasks.filter((task) => {
|
|
130
132
|
if (filters?.program && task.program !== filters.program) return false;
|
|
131
|
-
if (filters?.parentTaskId && task.parentTaskId !== filters.parentTaskId)
|
|
133
|
+
if (filters?.parentTaskId && task.parentTaskId !== filters.parentTaskId)
|
|
134
|
+
return false;
|
|
132
135
|
if (filters?.status && task.status !== filters.status) return false;
|
|
133
136
|
if (filters?.priority && task.priority !== filters.priority) return false;
|
|
134
137
|
return true;
|
|
@@ -137,7 +140,9 @@ export function listTasks(filters?: TaskFilters): Task[] {
|
|
|
137
140
|
|
|
138
141
|
export function getTask(program: string, taskId: string): Task | undefined {
|
|
139
142
|
const tasks = storeInstance().read();
|
|
140
|
-
return tasks.find(
|
|
143
|
+
return tasks.find(
|
|
144
|
+
(task) => taskKey(task.program, task.taskId) === taskKey(program, taskId),
|
|
145
|
+
);
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
export function createTask(input: {
|
|
@@ -152,7 +157,11 @@ export function createTask(input: {
|
|
|
152
157
|
const instance = storeInstance();
|
|
153
158
|
const tasks = instance.read();
|
|
154
159
|
|
|
155
|
-
const duplicate = tasks.find(
|
|
160
|
+
const duplicate = tasks.find(
|
|
161
|
+
(task) =>
|
|
162
|
+
taskKey(task.program, task.taskId) ===
|
|
163
|
+
taskKey(input.program, input.taskId),
|
|
164
|
+
);
|
|
156
165
|
if (duplicate) {
|
|
157
166
|
throw new Error(`Task already exists: [${input.program}][${input.taskId}]`);
|
|
158
167
|
}
|
|
@@ -172,7 +181,9 @@ export function createTask(input: {
|
|
|
172
181
|
|
|
173
182
|
tasks.push(task);
|
|
174
183
|
instance.write(tasks);
|
|
175
|
-
appendLog(
|
|
184
|
+
appendLog(
|
|
185
|
+
`[${task.program}][${task.taskId}] Task created with status=${task.status}`,
|
|
186
|
+
);
|
|
176
187
|
return task;
|
|
177
188
|
}
|
|
178
189
|
|
|
@@ -187,7 +198,11 @@ export function updateTask(input: {
|
|
|
187
198
|
}): Task | null {
|
|
188
199
|
const instance = storeInstance();
|
|
189
200
|
const tasks = instance.read();
|
|
190
|
-
const index = tasks.findIndex(
|
|
201
|
+
const index = tasks.findIndex(
|
|
202
|
+
(task) =>
|
|
203
|
+
taskKey(task.program, task.taskId) ===
|
|
204
|
+
taskKey(input.program, input.taskId),
|
|
205
|
+
);
|
|
191
206
|
|
|
192
207
|
if (index === -1) return null;
|
|
193
208
|
|
|
@@ -195,9 +210,13 @@ export function updateTask(input: {
|
|
|
195
210
|
const updated: Task = {
|
|
196
211
|
...existing,
|
|
197
212
|
...("parentTaskId" in input ? { parentTaskId: input.parentTaskId } : {}),
|
|
198
|
-
...("description" in input
|
|
213
|
+
...("description" in input
|
|
214
|
+
? { description: input.description ?? existing.description }
|
|
215
|
+
: {}),
|
|
199
216
|
...("status" in input ? { status: input.status ?? existing.status } : {}),
|
|
200
|
-
...("priority" in input
|
|
217
|
+
...("priority" in input
|
|
218
|
+
? { priority: input.priority ?? existing.priority }
|
|
219
|
+
: {}),
|
|
201
220
|
...("references" in input ? { references: input.references } : {}),
|
|
202
221
|
updatedAt: nowIso(),
|
|
203
222
|
};
|
|
@@ -211,7 +230,9 @@ export function updateTask(input: {
|
|
|
211
230
|
|
|
212
231
|
tasks[index] = updated;
|
|
213
232
|
instance.write(tasks);
|
|
214
|
-
appendLog(
|
|
233
|
+
appendLog(
|
|
234
|
+
`[${updated.program}][${updated.taskId}] Task updated status=${updated.status}`,
|
|
235
|
+
);
|
|
215
236
|
return updated;
|
|
216
237
|
}
|
|
217
238
|
|
|
@@ -226,7 +247,9 @@ export function markTaskDone(program: string, taskId: string): Task | null {
|
|
|
226
247
|
export function deleteTask(program: string, taskId: string): boolean {
|
|
227
248
|
const instance = storeInstance();
|
|
228
249
|
const tasks = instance.read();
|
|
229
|
-
const filtered = tasks.filter(
|
|
250
|
+
const filtered = tasks.filter(
|
|
251
|
+
(task) => taskKey(task.program, task.taskId) !== taskKey(program, taskId),
|
|
252
|
+
);
|
|
230
253
|
|
|
231
254
|
if (filtered.length === tasks.length) {
|
|
232
255
|
return false;
|
|
@@ -247,20 +270,27 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
247
270
|
return {
|
|
248
271
|
tool: {
|
|
249
272
|
task_create: tool({
|
|
250
|
-
description:
|
|
251
|
-
"Create a task tracked by [program][taskId]. Use this before running a cron cycle.",
|
|
273
|
+
description: `Create a task tracked by [program][taskId]. `,
|
|
252
274
|
args: {
|
|
253
|
-
program: tool.schema
|
|
254
|
-
|
|
275
|
+
program: tool.schema
|
|
276
|
+
.string()
|
|
277
|
+
.describe("Program name used for task and memory tags"),
|
|
278
|
+
taskId: tool.schema
|
|
279
|
+
.string()
|
|
280
|
+
.describe("Task identifier in snake_case"),
|
|
255
281
|
parentTaskId: tool.schema
|
|
256
282
|
.string()
|
|
257
283
|
.optional()
|
|
258
284
|
.describe("Optional parent task ID to create task hierarchies"),
|
|
259
|
-
description: tool.schema
|
|
285
|
+
description: tool.schema
|
|
286
|
+
.string()
|
|
287
|
+
.describe("Human description of the task"),
|
|
260
288
|
status: tool.schema
|
|
261
289
|
.string()
|
|
262
290
|
.optional()
|
|
263
|
-
.describe(
|
|
291
|
+
.describe(
|
|
292
|
+
"Optional status: pending, running, blocked, raised, done",
|
|
293
|
+
),
|
|
264
294
|
priority: tool.schema
|
|
265
295
|
.string()
|
|
266
296
|
.optional()
|
|
@@ -299,12 +329,24 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
299
329
|
}),
|
|
300
330
|
|
|
301
331
|
task_list: tool({
|
|
302
|
-
description:
|
|
332
|
+
description: `List tasks, optionally filtered by program/status/priority. ${OUTPUT_TAG_NOTE}`,
|
|
303
333
|
args: {
|
|
304
|
-
program: tool.schema
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
334
|
+
program: tool.schema
|
|
335
|
+
.string()
|
|
336
|
+
.optional()
|
|
337
|
+
.describe("Optional program filter"),
|
|
338
|
+
parentTaskId: tool.schema
|
|
339
|
+
.string()
|
|
340
|
+
.optional()
|
|
341
|
+
.describe("Optional parent task filter"),
|
|
342
|
+
status: tool.schema
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("Optional status filter"),
|
|
346
|
+
priority: tool.schema
|
|
347
|
+
.string()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Optional priority filter"),
|
|
308
350
|
},
|
|
309
351
|
async execute(args) {
|
|
310
352
|
const status = normalizeStatus(args.status);
|
|
@@ -331,7 +373,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
331
373
|
}),
|
|
332
374
|
|
|
333
375
|
task_get: tool({
|
|
334
|
-
description:
|
|
376
|
+
description: `Get a single task by [program][taskId]. ${OUTPUT_TAG_NOTE}`,
|
|
335
377
|
args: {
|
|
336
378
|
program: tool.schema.string().describe("Program name"),
|
|
337
379
|
taskId: tool.schema.string().describe("Task ID"),
|
|
@@ -346,7 +388,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
346
388
|
}),
|
|
347
389
|
|
|
348
390
|
task_update: tool({
|
|
349
|
-
description:
|
|
391
|
+
description: `Update task metadata or status. ${OUTPUT_TAG_NOTE}`,
|
|
350
392
|
args: {
|
|
351
393
|
program: tool.schema.string().describe("Program name"),
|
|
352
394
|
taskId: tool.schema.string().describe("Task ID"),
|
|
@@ -354,10 +396,19 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
354
396
|
.string()
|
|
355
397
|
.optional()
|
|
356
398
|
.describe("Updated parent task ID"),
|
|
357
|
-
description: tool.schema
|
|
399
|
+
description: tool.schema
|
|
400
|
+
.string()
|
|
401
|
+
.optional()
|
|
402
|
+
.describe("Updated description"),
|
|
358
403
|
status: tool.schema.string().optional().describe("Updated status"),
|
|
359
|
-
priority: tool.schema
|
|
360
|
-
|
|
404
|
+
priority: tool.schema
|
|
405
|
+
.string()
|
|
406
|
+
.optional()
|
|
407
|
+
.describe("Updated priority"),
|
|
408
|
+
references: tool.schema
|
|
409
|
+
.string()
|
|
410
|
+
.optional()
|
|
411
|
+
.describe("Updated references"),
|
|
361
412
|
},
|
|
362
413
|
async execute(args) {
|
|
363
414
|
const status = normalizeStatus(args.status);
|
|
@@ -410,7 +461,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
410
461
|
}),
|
|
411
462
|
|
|
412
463
|
task_mark_done: tool({
|
|
413
|
-
description:
|
|
464
|
+
description: `Mark a task as done after it is resolved. ${OUTPUT_TAG_NOTE}`,
|
|
414
465
|
args: {
|
|
415
466
|
program: tool.schema.string().describe("Program name"),
|
|
416
467
|
taskId: tool.schema.string().describe("Task ID"),
|
|
@@ -425,7 +476,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
425
476
|
}),
|
|
426
477
|
|
|
427
478
|
task_delete: tool({
|
|
428
|
-
description:
|
|
479
|
+
description: `Delete a task from the task manager. ${OUTPUT_TAG_NOTE}`,
|
|
429
480
|
args: {
|
|
430
481
|
program: tool.schema.string().describe("Program name"),
|
|
431
482
|
taskId: tool.schema.string().describe("Task ID"),
|
|
@@ -440,7 +491,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
|
|
|
440
491
|
}),
|
|
441
492
|
|
|
442
493
|
task_store_info: tool({
|
|
443
|
-
description:
|
|
494
|
+
description: `Get task store file path and current task count. ${OUTPUT_TAG_NOTE}`,
|
|
444
495
|
args: {},
|
|
445
496
|
async execute() {
|
|
446
497
|
const tasks = listTasks();
|