opencode-jobs 0.2.0 → 1.0.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/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { Hooks } from "@opencode-ai/plugin";
2
- declare const _default: () => Promise<Hooks>;
2
+ declare const _default: (input: import("@opencode-ai/plugin").PluginInput) => Promise<Hooks>;
3
3
  export default _default;
package/dist/index.js CHANGED
@@ -149,31 +149,31 @@ function stateRoot() {
149
149
  return process.env.XDG_STATE_HOME ?? path.join(homedir(), ".local", "state");
150
150
  }
151
151
  function worktreesDirectory(scopeId) {
152
- return path.join(stateRoot(), "opencode", "scheduler", "worktrees", scopeId);
152
+ return path.join(stateRoot(), "opencode", "jobs", "worktrees", scopeId);
153
153
  }
154
- function schedulerDirectory() {
155
- return path.join(configRoot(), "opencode", "scheduler");
154
+ function jobsStateDirectory() {
155
+ return path.join(configRoot(), "opencode", "jobs");
156
156
  }
157
157
  function registryPath() {
158
- return path.join(schedulerDirectory(), "registry.json");
158
+ return path.join(jobsStateDirectory(), "registry.json");
159
159
  }
160
160
  function scopeDirectory(scopeId) {
161
- return path.join(schedulerDirectory(), "scopes", scopeId);
161
+ return path.join(jobsStateDirectory(), "scopes", scopeId);
162
162
  }
163
163
  function runsDirectory(scopeId) {
164
- return path.join(schedulerDirectory(), "runs", scopeId);
164
+ return path.join(jobsStateDirectory(), "runs", scopeId);
165
165
  }
166
166
  function runsFile(scopeId, slug) {
167
167
  return path.join(runsDirectory(scopeId), `${slug}.jsonl`);
168
168
  }
169
169
  function sessionStateDirectory(scopeId) {
170
- return path.join(schedulerDirectory(), "sessions", scopeId);
170
+ return path.join(jobsStateDirectory(), "sessions", scopeId);
171
171
  }
172
172
  function sessionStateFile(scopeId, slug) {
173
173
  return path.join(sessionStateDirectory(scopeId), `${slug}.txt`);
174
174
  }
175
175
  function logDirectory(scopeId) {
176
- return path.join(configRoot(), "opencode", "logs", "scheduler", scopeId);
176
+ return path.join(configRoot(), "opencode", "logs", "jobs", scopeId);
177
177
  }
178
178
  function logFile(scopeId, slug) {
179
179
  return path.join(logDirectory(scopeId), `${slug}.log`);
@@ -182,7 +182,7 @@ function systemdUserDirectory() {
182
182
  return path.join(configRoot(), "systemd", "user");
183
183
  }
184
184
  function jobsDirectory(workdir) {
185
- return path.join(workdir, ".opencode", "scheduler", "jobs");
185
+ return path.join(workdir, ".opencode", "jobs");
186
186
  }
187
187
  function unitBase(scopeId, slug) {
188
188
  return `opencode-sched-${scopeId}-${slug}`;
@@ -447,19 +447,22 @@ var registryFileSchema = z2.object({
447
447
  version: z2.literal(1),
448
448
  projects: z2.record(z2.string(), z2.unknown())
449
449
  });
450
+ function readRegistryFile(file) {
451
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
452
+ const result = registryFileSchema.safeParse(parsed);
453
+ if (!result.success)
454
+ throw new Error(`Invalid job registry: ${file}`);
455
+ const projects = {};
456
+ for (const [key, value] of Object.entries(result.data.projects)) {
457
+ const entry = registryEntrySchema.safeParse(value);
458
+ if (entry.success)
459
+ projects[key] = entry.data;
460
+ }
461
+ return { version: 1, projects };
462
+ }
450
463
  function loadRegistry() {
451
464
  try {
452
- const parsed = JSON.parse(readFileSync2(registryPath(), "utf8"));
453
- const result = registryFileSchema.safeParse(parsed);
454
- if (!result.success)
455
- return { version: 1, projects: {} };
456
- const projects = {};
457
- for (const [key, value] of Object.entries(result.data.projects)) {
458
- const entry = registryEntrySchema.safeParse(value);
459
- if (entry.success)
460
- projects[key] = entry.data;
461
- }
462
- return { version: 1, projects };
465
+ return readRegistryFile(registryPath());
463
466
  } catch {
464
467
  return { version: 1, projects: {} };
465
468
  }
@@ -548,7 +551,7 @@ import { spawnSync } from "child_process";
548
551
  import path4 from "path";
549
552
  import { homedir as homedir2 } from "os";
550
553
  function findOpencode() {
551
- const override = process.env.OPENCODE_SCHEDULER_OPENCODE_PATH;
554
+ const override = process.env.OPENCODE_JOBS_OPENCODE_PATH ?? process.env.OPENCODE_SCHEDULER_OPENCODE_PATH;
552
555
  if (override !== undefined && override.length > 0)
553
556
  return override;
554
557
  const which = spawnSync("sh", ["-c", "command -v opencode"], {
@@ -581,24 +584,24 @@ function guardScriptLines(guard) {
581
584
  ];
582
585
  }
583
586
  function worktreeDefaultRoot(scopeId) {
584
- return "${XDG_STATE_HOME:-$HOME/.local/state}/opencode/scheduler/worktrees/" + scopeId;
587
+ return "${XDG_STATE_HOME:-$HOME/.local/state}/opencode/jobs/worktrees/" + scopeId;
585
588
  }
586
589
  function worktreePrologueLines(job, scopeId) {
587
590
  const base = job.worktree?.base;
588
591
  return [
589
592
  "wt_enabled=1",
590
593
  'orig_pwd="$(pwd)"',
591
- 'lock_dir="$config_root/opencode/scheduler/locks/$scope"',
594
+ 'lock_dir="$config_root/opencode/jobs/locks/$scope"',
592
595
  'mkdir -p "$lock_dir"',
593
596
  'exec 9>"$lock_dir/$slug.lock"',
594
597
  "if ! flock -n 9; then",
595
- ' echo "scheduler: another run of $slug is already active, skipping"',
598
+ ' echo "opencode-jobs: another run of $slug is already active, skipping"',
596
599
  " finish skipped 0",
597
600
  " exit 0",
598
601
  "fi",
599
602
  base === undefined ? `wt_root="${worktreeDefaultRoot(scopeId)}"` : `wt_root=${shQuote(base)}`,
600
603
  'if ! mkdir -p "$wt_root"; then',
601
- ' echo "scheduler: cannot create worktree base $wt_root"',
604
+ ' echo "opencode-jobs: cannot create worktree base $wt_root"',
602
605
  " finish failed 1",
603
606
  " exit 1",
604
607
  "fi",
@@ -613,31 +616,31 @@ function worktreePrologueLines(job, scopeId) {
613
616
  " wt_stale_saved=1",
614
617
  ' git -C "$wt_path" add -A >/dev/null 2>&1 || wt_stale_saved=0',
615
618
  ' if [ "$wt_stale_saved" -eq 1 ] && ! git -C "$wt_path" diff --cached --quiet >/dev/null 2>&1; then',
616
- ' git -C "$wt_path" -c user.name=opencode-jobs -c user.email=scheduler@opencode.invalid commit --no-gpg-sign -m "opencode-jobs: $slug recovery (stale worktree)" >/dev/null 2>&1 || wt_stale_saved=0',
619
+ ' git -C "$wt_path" -c user.name=opencode-jobs -c user.email=jobs@opencode.invalid commit --no-gpg-sign -m "opencode-jobs: $slug recovery (stale worktree)" >/dev/null 2>&1 || wt_stale_saved=0',
617
620
  " fi",
618
621
  ' if [ "$wt_stale_saved" -eq 1 ]; then',
619
622
  ' git worktree remove --force "$wt_path" >/dev/null 2>&1',
620
623
  ' rm -rf "$wt_path"',
621
624
  " else",
622
- ' echo "scheduler: cannot save changes in stale worktree $wt_path; keeping it and aborting this run"',
625
+ ' echo "opencode-jobs: cannot save changes in stale worktree $wt_path; keeping it and aborting this run"',
623
626
  ' wt_branch=""',
624
627
  " finish failed 1",
625
628
  " exit 1",
626
629
  " fi",
627
630
  " else",
628
- ' echo "scheduler: removing unexpected directory at $wt_path"',
631
+ ' echo "opencode-jobs: removing unexpected directory at $wt_path"',
629
632
  ' rm -rf "$wt_path"',
630
633
  " fi",
631
634
  " git worktree prune >/dev/null 2>&1",
632
635
  "fi",
633
636
  'if ! git worktree add -b "$wt_branch" "$wt_path" "$wt_base_ref"; then',
634
- ' echo "scheduler: failed to create worktree $wt_path (worktree jobs require a git repository)"',
637
+ ' echo "opencode-jobs: failed to create worktree $wt_path (worktree jobs require a git repository)"',
635
638
  ' wt_branch=""',
636
639
  " finish failed 1",
637
640
  " exit 1",
638
641
  "fi",
639
642
  'if [ -n "$wt_sub" ] && [ ! -d "$wt_path/$wt_sub" ]; then',
640
- ' echo "scheduler: project subdirectory $wt_sub is missing from the worktree at $wt_base_ref"',
643
+ ' echo "opencode-jobs: project subdirectory $wt_sub is missing from the worktree at $wt_base_ref"',
641
644
  ' git -C "$orig_pwd" worktree remove --force "$wt_path" >/dev/null 2>&1',
642
645
  ' rm -rf "$wt_path"',
643
646
  ' wt_branch=""',
@@ -659,10 +662,10 @@ function worktreeEpilogueLines(options) {
659
662
  ' git -C "$wt_path" add -A >/dev/null 2>&1',
660
663
  " wt_keep=0",
661
664
  ' if ! git -C "$wt_path" diff --cached --quiet >/dev/null 2>&1; then',
662
- ' if git -C "$wt_path" -c user.name=opencode-jobs -c user.email=scheduler@opencode.invalid commit --no-gpg-sign -m "$wt_msg" >/dev/null 2>&1; then',
663
- ' echo "scheduler: committed worktree changes to branch $wt_branch"',
665
+ ' if git -C "$wt_path" -c user.name=opencode-jobs -c user.email=jobs@opencode.invalid commit --no-gpg-sign -m "$wt_msg" >/dev/null 2>&1; then',
666
+ ' echo "opencode-jobs: committed worktree changes to branch $wt_branch"',
664
667
  " else",
665
- ' echo "scheduler: worktree commit failed, keeping worktree at $wt_path"',
668
+ ' echo "opencode-jobs: worktree commit failed, keeping worktree at $wt_path"',
666
669
  " wt_keep=1",
667
670
  " fi",
668
671
  " fi",
@@ -688,7 +691,7 @@ function compactSessionLines() {
688
691
  "compact_session() {",
689
692
  ' csid="$1"',
690
693
  " if ! command -v curl >/dev/null 2>&1; then",
691
- ' echo "scheduler: curl not available, skipping compaction"',
694
+ ' echo "opencode-jobs: curl not available, skipping compaction"',
692
695
  " return 0",
693
696
  " fi",
694
697
  ' serve_out="$(mktemp)"',
@@ -705,7 +708,7 @@ function compactSessionLines() {
705
708
  " tries=$((tries + 1))",
706
709
  " done",
707
710
  ' if [ -z "$serve_port" ]; then',
708
- ' echo "scheduler: compaction server failed to start"',
711
+ ' echo "opencode-jobs: compaction server failed to start"',
709
712
  ' sed -n "1,10p" "$serve_err" >&2',
710
713
  ' kill "$serve_pid" 2>/dev/null',
711
714
  ' wait "$serve_pid" 2>/dev/null',
@@ -724,7 +727,7 @@ function compactSessionLines() {
724
727
  " tries=$((tries + 1))",
725
728
  " done",
726
729
  ' if [ "$healthy" -ne 1 ]; then',
727
- ' echo "scheduler: compaction server never became healthy"',
730
+ ' echo "opencode-jobs: compaction server never became healthy"',
728
731
  ' kill "$serve_pid" 2>/dev/null',
729
732
  ' wait "$serve_pid" 2>/dev/null',
730
733
  ' rm -f "$serve_out" "$serve_err"',
@@ -744,22 +747,22 @@ function compactSessionLines() {
744
747
  String.raw` cs_model="$(printf '%s\n' "$defaults" | sed -n '${DEFAULT_MODEL_SED}' | head -n 1)"`,
745
748
  " fi",
746
749
  ' if [ -z "$cs_provider" ] || [ -z "$cs_model" ]; then',
747
- ' echo "scheduler: could not resolve a model for compaction, skipping"',
750
+ ' echo "opencode-jobs: could not resolve a model for compaction, skipping"',
748
751
  ' kill "$serve_pid" 2>/dev/null',
749
752
  ' wait "$serve_pid" 2>/dev/null',
750
753
  ' rm -f "$serve_out" "$serve_err"',
751
754
  " return 0",
752
755
  " fi",
753
- ' echo "scheduler: compacting session $csid (mode: $session_mode)"',
756
+ ' echo "opencode-jobs: compacting session $csid (mode: $session_mode)"',
754
757
  String.raw` result="$(curl -s --max-time 900 -X POST -H 'content-type: application/json' -d "{\"providerID\":\"$cs_provider\",\"modelID\":\"$cs_model\"}" "http://127.0.0.1:$serve_port/session/$csid/summarize")"`,
755
758
  ' if [ "$result" != "true" ]; then',
756
- ' echo "scheduler: compaction failed: $result"',
759
+ ' echo "opencode-jobs: compaction failed: $result"',
757
760
  " fi",
758
761
  ' if [ "$result" = "true" ] && [ "$oc_keep_last" -eq 1 ] && [ -n "$cs_text" ]; then',
759
762
  String.raw` inject_body="{\"noReply\":true,\"parts\":[{\"type\":\"text\",\"text\":\"$cs_text\"}]}"`,
760
763
  ` http="$(curl -s -o /dev/null -w '%{http_code}' --max-time 120 -X POST -H 'content-type: application/json' -d "$inject_body" "http://127.0.0.1:$serve_port/session/$csid/message")"`,
761
764
  ' if [ "$http" != "200" ]; then',
762
- ' echo "scheduler: keeping last result failed (HTTP $http)"',
765
+ ' echo "opencode-jobs: keeping last result failed (HTTP $http)"',
763
766
  " fi",
764
767
  " fi",
765
768
  ' kill "$serve_pid" 2>/dev/null',
@@ -780,25 +783,25 @@ function runScriptContent(job, scopeId, opencodeBin) {
780
783
  `scope=${shQuote(scopeId)}`,
781
784
  `oc_bin=${shQuote(opencodeBin)}`,
782
785
  'config_root="${XDG_CONFIG_HOME:-$HOME/.config}"',
783
- 'runs="$config_root/opencode/scheduler/runs/$scope"',
786
+ 'runs="$config_root/opencode/jobs/runs/$scope"',
784
787
  'mkdir -p "$runs"',
785
788
  'record_file="$runs/$slug.jsonl"',
786
789
  ...isTracked ? [
787
- 'sessions="$config_root/opencode/scheduler/sessions/$scope"',
790
+ 'sessions="$config_root/opencode/jobs/sessions/$scope"',
788
791
  'mkdir -p "$sessions"',
789
792
  'state_file="$sessions/$slug.txt"',
790
793
  `session_mode=${shQuote(mode)}`,
791
794
  'prev_session=""',
792
795
  'if [ -f "$state_file" ]; then prev_session=$(cat "$state_file"); fi'
793
796
  ] : [],
794
- 'started_by="${OPENCODE_SCHEDULER_STARTED_BY:-scheduled}"',
797
+ 'started_by="${OPENCODE_JOBS_STARTED_BY:-scheduled}"',
795
798
  'run_id="$(date +%s%N)-$$"',
796
799
  "started=$(date +%s)",
797
800
  'new_session=""',
798
801
  'wt_branch=""',
799
802
  'wt_commit=""',
800
803
  `export OPENCODE_PERMISSION='{"question":"deny"}'`,
801
- 'export OPENCODE_SCHEDULER_RUN_ID="$run_id"',
804
+ 'export OPENCODE_JOBS_RUN_ID="$run_id"',
802
805
  "finish() {",
803
806
  ' status="$1"',
804
807
  ' code="$2"',
@@ -839,7 +842,7 @@ function runScriptContent(job, scopeId, opencodeBin) {
839
842
  'cat "$json_out"',
840
843
  ...extractSessionIdLines("new_session"),
841
844
  'if [ "$code" -ne 0 ] && [ -n "$prev_session" ] && [ -z "$new_session" ] && grep -qi "session not found" "$json_out"; then',
842
- ' echo "scheduler: session $prev_session not found, retrying with a fresh session"',
845
+ ' echo "opencode-jobs: session $prev_session not found, retrying with a fresh session"',
843
846
  ' rm -f "$json_out"',
844
847
  ' json_out="$(mktemp)"',
845
848
  ' run_opencode "" 1 >"$json_out" 2>&1',
@@ -1203,7 +1206,7 @@ function scheduleJobOutput(input, directory) {
1203
1206
  updatedAt: nowIso()
1204
1207
  };
1205
1208
  saveJob(directory, job);
1206
- const relativePath = `.opencode/scheduler/jobs/${slug}.json`;
1209
+ const relativePath = `.opencode/jobs/${slug}.json`;
1207
1210
  const lines = [
1208
1211
  `${existing.ok ? "Updated" : "Created"} job "${job.name}" (${slug})`,
1209
1212
  `Definition: ${relativePath} (${job.schedule} \u2014 ${describeCron(sets)})`
@@ -1259,7 +1262,7 @@ function showJobOutput(slugInput, directory) {
1259
1262
  const lines = [
1260
1263
  `${job.name} (${job.slug})`,
1261
1264
  `Schedule: ${job.schedule} \u2014 ${describeCron(sets)}`,
1262
- `Definition: .opencode/scheduler/jobs/${job.slug}.json (updated ${job.updatedAt})`,
1265
+ `Definition: .opencode/jobs/${job.slug}.json (updated ${job.updatedAt})`,
1263
1266
  `Run: ${runDesc}`
1264
1267
  ];
1265
1268
  if (job.guard !== undefined)
@@ -1309,9 +1312,7 @@ function removeJobDefinitionOutput(slugInput, directory) {
1309
1312
  if (!existsSync4(file))
1310
1313
  return fail(`No job "${slug}" in ${jobsDirectory(directory)}`);
1311
1314
  rmSync2(file);
1312
- const lines = [
1313
- `Deleted job definition .opencode/scheduler/jobs/${slug}.json`
1314
- ];
1315
+ const lines = [`Deleted job definition .opencode/jobs/${slug}.json`];
1315
1316
  const scopeId = registryEntry(directory)?.scopeId ?? deriveScopeId(directory);
1316
1317
  const state = sessionStateFile(scopeId, slug);
1317
1318
  if (existsSync4(state)) {
@@ -1359,7 +1360,7 @@ function runJobNowOutput(slugInput, directory) {
1359
1360
  const fd = openSync(log, "a");
1360
1361
  const child = spawn("/bin/sh", [script], {
1361
1362
  cwd: path6.resolve(directory),
1362
- env: { ...process.env, OPENCODE_SCHEDULER_STARTED_BY: "manual" },
1363
+ env: { ...process.env, OPENCODE_JOBS_STARTED_BY: "manual" },
1363
1364
  stdio: ["ignore", fd, fd]
1364
1365
  });
1365
1366
  child.unref();
@@ -1393,7 +1394,7 @@ function listProjectsOutput() {
1393
1394
  const entries = Object.values(registry.projects).toSorted((a, b) => a.workdir.localeCompare(b.workdir));
1394
1395
  if (entries.length === 0)
1395
1396
  return ok("No projects with scheduled jobs are registered.");
1396
- const lines = ["Registry: ~/.config/opencode/scheduler/registry.json"];
1397
+ const lines = ["Registry: ~/.config/opencode/jobs/registry.json"];
1397
1398
  for (const entry of entries) {
1398
1399
  const missing = existsSync4(entry.workdir) ? "" : " [WORKDIR MISSING]";
1399
1400
  lines.push(`- ${entry.workdir}${missing}`, ` scope ${entry.scopeId}, ${String(entry.jobs.length)} job(s): ${entry.jobs.join(", ")}`);
@@ -1402,12 +1403,12 @@ function listProjectsOutput() {
1402
1403
  `));
1403
1404
  }
1404
1405
  var listJobsTool = tool({
1405
- description: "List scheduled job definitions for the current project (from .opencode/scheduler/jobs/), including enabled state, next run, and last run status.",
1406
+ description: "List scheduled job definitions for the current project (from .opencode/jobs/), including enabled state, next run, and last run status.",
1406
1407
  args: {},
1407
1408
  execute: (_input, context) => Promise.resolve(listJobsOutput(context.directory))
1408
1409
  });
1409
1410
  var scheduleJobTool = tool({
1410
- description: "Create or update a scheduled job definition in the current project (.opencode/scheduler/jobs/<slug>.json, git-committable). Schedule is a 5-field cron expression. Set either prompt (natural language) or command (custom command name). If the project is enabled, systemd units are re-synced automatically.",
1411
+ description: "Create or update a scheduled job definition in the current project (.opencode/jobs/<slug>.json, git-committable). Schedule is a 5-field cron expression. Set either prompt (natural language) or command (custom command name). If the project is enabled, systemd units are re-synced automatically.",
1411
1412
  args: {
1412
1413
  name: tool.schema.string().describe("Human-readable job name"),
1413
1414
  schedule: tool.schema.string().describe('5-field cron expression, e.g. "0 9 * * *" (daily 9am), "0 */6 * * *" (every 6h), "30 8 * * 1" (Mon 8:30)'),
@@ -1417,7 +1418,7 @@ var scheduleJobTool = tool({
1417
1418
  session: tool.schema.string().optional().describe(`Session continuity between runs: "new" (default, fresh session each run), "persist" (continue the same session), "compact" (continue the same session; after each run the history is compacted into a summary the next run starts from), "compact+last" (like compact, but the run's final result message is re-injected after the summary so the next run starts from summary plus last result)`),
1418
1419
  guard: tool.schema.string().optional().describe('Shell command run before the job; the run only starts if it exits 0, otherwise it is recorded as skipped (applies to run_job too). E.g. "! git diff --quiet" to run only when the repo has changes'),
1419
1420
  worktree: tool.schema.boolean().optional().describe("Run the job in a fresh git worktree instead of the project checkout: the worktree is created from worktreeRef (default HEAD), the job runs inside it, all changes are committed to a per-run branch opencode-jobs/<slug>/\u2026, and the worktree is removed afterwards (kept if the safety commit fails). Requires the project to be a git repository"),
1420
- worktreeBase: tool.schema.string().optional().describe("Parent directory for the worktree (default: ~/.local/state/opencode/scheduler/worktrees/<scopeId>/<slug>). Relative paths resolve against the project directory; it should be dedicated to scheduler worktrees"),
1421
+ worktreeBase: tool.schema.string().optional().describe("Parent directory for the worktree (default: ~/.local/state/opencode/jobs/worktrees/<scopeId>/<slug>). Relative paths resolve against the project directory; it should be dedicated to job worktrees"),
1421
1422
  worktreeRef: tool.schema.string().optional().describe('Git ref the worktree branch starts from (default: "HEAD")'),
1422
1423
  worktreeCommitMessage: tool.schema.string().optional().describe('Commit message used when saving worktree changes (default: "opencode-jobs: <slug> run <runId>")'),
1423
1424
  agent: tool.schema.string().optional().describe("Agent to use for the run"),
@@ -1457,7 +1458,7 @@ var jobLogsTool = tool({
1457
1458
  execute: (input, context) => Promise.resolve(jobLogsOutput(input.slug, input.lines, context.directory))
1458
1459
  });
1459
1460
  var enableProjectTool = tool({
1460
- description: "Enable scheduled jobs for the current project: installs a systemd user service+timer per job definition in .opencode/scheduler/jobs/, registers the project in the global registry (~/.config/opencode/scheduler/registry.json), and removes stale units for deleted jobs. Idempotent, so it also re-syncs after job definitions change. Linux only.",
1461
+ description: "Enable scheduled jobs for the current project: installs a systemd user service+timer per job definition in .opencode/jobs/, registers the project in the global registry (~/.config/opencode/jobs/registry.json), and removes stale units for deleted jobs. Idempotent, so it also re-syncs after job definitions change. Linux only.",
1461
1462
  args: {},
1462
1463
  execute: (_input, context) => Promise.resolve(enableProjectOutput(context.directory))
1463
1464
  });
@@ -1481,11 +1482,11 @@ function disableProjectOutput(directory) {
1481
1482
  }
1482
1483
  }
1483
1484
  var listProjectsTool = tool({
1484
- description: "List all projects with enabled scheduled jobs from the global registry (~/.config/opencode/scheduler/registry.json).",
1485
+ description: "List all projects with enabled scheduled jobs from the global registry (~/.config/opencode/jobs/registry.json).",
1485
1486
  args: {},
1486
1487
  execute: () => Promise.resolve(listProjectsOutput())
1487
1488
  });
1488
- var schedulerTools = {
1489
+ var jobsTools = {
1489
1490
  schedule_job: scheduleJobTool,
1490
1491
  list_jobs: listJobsTool,
1491
1492
  get_job: showJobTool,
@@ -1497,12 +1498,102 @@ var schedulerTools = {
1497
1498
  list_projects: listProjectsTool
1498
1499
  };
1499
1500
 
1501
+ // src/migration.ts
1502
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, renameSync as renameSync2, rmdirSync } from "fs";
1503
+ import path7 from "path";
1504
+ function legacyJobsStateDirectory() {
1505
+ return path7.join(configRoot(), "opencode", "scheduler");
1506
+ }
1507
+ function legacyRegistryPath() {
1508
+ return path7.join(legacyJobsStateDirectory(), "registry.json");
1509
+ }
1510
+ function legacyDefinitionsDirectory(workdir) {
1511
+ return path7.join(workdir, ".opencode", "scheduler", "jobs");
1512
+ }
1513
+ function migrationMoves(projects) {
1514
+ return [
1515
+ {
1516
+ from: legacyJobsStateDirectory(),
1517
+ to: jobsStateDirectory()
1518
+ },
1519
+ {
1520
+ from: path7.join(configRoot(), "opencode", "logs", "scheduler"),
1521
+ to: path7.join(configRoot(), "opencode", "logs", "jobs")
1522
+ },
1523
+ {
1524
+ from: path7.join(stateRoot(), "opencode", "scheduler", "worktrees"),
1525
+ to: path7.join(stateRoot(), "opencode", "jobs", "worktrees")
1526
+ },
1527
+ ...[...projects].map((workdir) => ({
1528
+ from: legacyDefinitionsDirectory(workdir),
1529
+ to: jobsDirectory(workdir)
1530
+ }))
1531
+ ];
1532
+ }
1533
+ function removeLegacyProjectDirectory(workdir) {
1534
+ try {
1535
+ rmdirSync(path7.join(workdir, ".opencode", "scheduler"));
1536
+ } catch {}
1537
+ }
1538
+ function migrateStorage(projectDirectory, shouldResync) {
1539
+ const project = path7.resolve(projectDirectory);
1540
+ const legacyRegistry = legacyRegistryPath();
1541
+ const canonicalRegistry = path7.join(jobsStateDirectory(), "registry.json");
1542
+ const registryFile = existsSync5(legacyRegistry) ? legacyRegistry : canonicalRegistry;
1543
+ const registry = existsSync5(registryFile) ? readRegistryFile(registryFile) : { version: 1, projects: {} };
1544
+ const registeredProjects = new Set(Object.keys(registry.projects));
1545
+ const projects = new Set([project, ...registeredProjects]);
1546
+ const moves = migrationMoves(projects).filter(({ from }) => existsSync5(from));
1547
+ for (const { from, to } of moves) {
1548
+ if (existsSync5(to)) {
1549
+ throw new Error(`Cannot migrate legacy job storage because both paths exist: ${from} and ${to}. Reconcile or back up one path, then retry; neither path was changed.`);
1550
+ }
1551
+ }
1552
+ for (const { from, to } of moves) {
1553
+ mkdirSync4(path7.dirname(to), { recursive: true });
1554
+ renameSync2(from, to);
1555
+ }
1556
+ for (const workdir of projects)
1557
+ removeLegacyProjectDirectory(workdir);
1558
+ const result = {
1559
+ moved: moves,
1560
+ resyncedProjects: [],
1561
+ warnings: []
1562
+ };
1563
+ if (!shouldResync || moves.length === 0)
1564
+ return result;
1565
+ for (const workdir of registeredProjects) {
1566
+ try {
1567
+ enableProject(workdir);
1568
+ result.resyncedProjects.push(workdir);
1569
+ } catch (error) {
1570
+ const message = error instanceof Error ? error.message : String(error);
1571
+ result.warnings.push(`${workdir}: ${message}`);
1572
+ }
1573
+ }
1574
+ return result;
1575
+ }
1576
+
1500
1577
  // src/index.ts
1501
- var src_default = () => {
1578
+ var src_default = (input) => {
1579
+ if (typeof input.directory === "string") {
1580
+ try {
1581
+ const migration = migrateStorage(input.directory, true);
1582
+ if (migration.moved.length > 0) {
1583
+ console.error(`[opencode-jobs] migrated ${String(migration.moved.length)} legacy storage location(s) to jobs paths`);
1584
+ }
1585
+ for (const warning of migration.warnings) {
1586
+ console.error(`[opencode-jobs] migration warning: ${warning}`);
1587
+ }
1588
+ } catch (error) {
1589
+ const message = error instanceof Error ? error.message : String(error);
1590
+ console.error(`[opencode-jobs] storage migration failed: ${message}`);
1591
+ }
1592
+ }
1502
1593
  if (process.platform !== "linux") {
1503
1594
  console.error("[opencode-jobs] warning: this is not a Linux host, systemd user timers are unavailable \u2014 scheduling tools will not work here");
1504
1595
  }
1505
- return Promise.resolve({ tool: schedulerTools });
1596
+ return Promise.resolve({ tool: jobsTools });
1506
1597
  };
1507
1598
  export {
1508
1599
  src_default as default
package/dist/install.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type StorageMigration } from "./migration.js";
1
2
  export declare const PACKAGE_NAME = "opencode-jobs";
2
3
  export declare const SKILL_NAME = "opencode-jobs";
3
4
  export type ConfigInstall = {
@@ -15,6 +16,7 @@ export interface ProjectInstall {
15
16
  projectDirectory: string;
16
17
  plugin: ConfigInstall;
17
18
  skill: SkillInstall;
19
+ migration?: StorageMigration;
18
20
  }
19
21
  export declare function installPluginConfig(projectDirectory: string): ConfigInstall;
20
22
  export declare function installSkill(projectDirectory: string, packageDirectory: string): SkillInstall;
@@ -39,6 +41,7 @@ export interface ProjectUninstall {
39
41
  plugin: ConfigUninstall;
40
42
  skill: SkillUninstall;
41
43
  purge?: PurgeResult;
44
+ migration?: StorageMigration;
42
45
  }
43
46
  export declare function uninstallPluginConfig(projectDirectory: string): ConfigUninstall;
44
47
  export declare function uninstallSkill(projectDirectory: string, packageDirectory: string): SkillUninstall;
@@ -0,0 +1,9 @@
1
+ export interface StorageMigration {
2
+ moved: {
3
+ from: string;
4
+ to: string;
5
+ }[];
6
+ resyncedProjects: string[];
7
+ warnings: string[];
8
+ }
9
+ export declare function migrateStorage(projectDirectory: string, shouldResync: boolean): StorageMigration;
package/dist/paths.d.ts CHANGED
@@ -2,7 +2,7 @@ export declare function configRoot(): string;
2
2
  export declare function stateRoot(): string;
3
3
  export declare function worktreesDirectory(scopeId: string): string;
4
4
  export declare function locksDirectory(scopeId: string): string;
5
- export declare function schedulerDirectory(): string;
5
+ export declare function jobsStateDirectory(): string;
6
6
  export declare function registryPath(): string;
7
7
  export declare function scopeDirectory(scopeId: string): string;
8
8
  export declare function runsDirectory(scopeId: string): string;
@@ -9,6 +9,7 @@ export interface Registry {
9
9
  version: 1;
10
10
  projects: Record<string, RegistryEntry>;
11
11
  }
12
+ export declare function readRegistryFile(file: string): Registry;
12
13
  export declare function loadRegistry(): Registry;
13
14
  export declare function saveRegistry(registry: Registry): void;
14
15
  export declare function registryEntry(workdir: string): RegistryEntry | undefined;
package/dist/tools.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import { type ToolDefinition } from "@opencode-ai/plugin";
2
- export declare const schedulerTools: Record<string, ToolDefinition>;
2
+ export declare const jobsTools: Record<string, ToolDefinition>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-jobs",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "opencode plugin that schedules recurring agent jobs as systemd user timers, with git-committable job definitions, run history, and session continuity",
5
5
  "type": "module",
6
6
  "engines": {
@@ -7,13 +7,13 @@ compatibility: opencode on Linux with systemd
7
7
 
8
8
  # OpenCode Jobs
9
9
 
10
- Use the `opencode-jobs` tools instead of editing systemd units or global
11
- scheduler state directly.
10
+ Use the `opencode-jobs` tools instead of editing systemd units or global job
11
+ state directly.
12
12
 
13
13
  ## Workflow
14
14
 
15
15
  1. Create or update a definition with `schedule_job`. Definitions belong in
16
- `.opencode/scheduler/jobs/<slug>.json` and should be committed with the
16
+ `.opencode/jobs/<slug>.json` and should be committed with the
17
17
  project.
18
18
  2. Inspect definitions with `list_jobs` or `get_job`.
19
19
  3. Run `enable_project` after the first job is created. Run it again to
@@ -36,7 +36,7 @@ scheduler state directly.
36
36
  - A `guard` is a shell command that must exit zero for the job to run.
37
37
  - Set `worktree: true` when the job should not touch the user's checkout:
38
38
  each run gets a fresh git worktree (default base
39
- `~/.local/state/opencode/scheduler/worktrees/…`, override with
39
+ `~/.local/state/opencode/jobs/worktrees/…`, override with
40
40
  `worktree.base`), all changes are committed to a per-run branch
41
41
  `opencode-jobs/<slug>/…`, and the worktree is removed. Overlapping runs
42
42
  of the same job are skipped via a lock; subdirectory projects run in