@youtyan/code-viewer 0.6.3 → 0.6.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.
@@ -823,7 +823,7 @@ function isCommandNotFoundMessage(command, message) {
823
823
  }
824
824
  var EXTERNAL_COMMAND_NAMES, commandNameSet, activeOverrides;
825
825
  var init_command_resolver = __esm(() => {
826
- EXTERNAL_COMMAND_NAMES = ["git", "rg", "docker"];
826
+ EXTERNAL_COMMAND_NAMES = ["git", "rg", "docker", "gh"];
827
827
  commandNameSet = new Set(EXTERNAL_COMMAND_NAMES);
828
828
  activeOverrides = new Map;
829
829
  });
@@ -2231,6 +2231,15 @@ function takeValue(argv, index, flag) {
2231
2231
  function shellSingleQuote(value) {
2232
2232
  return `'${value.replace(/'/g, "'\\''")}'`;
2233
2233
  }
2234
+ async function readStdin() {
2235
+ if (process.stdin.isTTY)
2236
+ return "";
2237
+ const chunks = [];
2238
+ for await (const chunk of process.stdin) {
2239
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2240
+ }
2241
+ return Buffer.concat(chunks).toString("utf8");
2242
+ }
2234
2243
  function isUnsafeText(value) {
2235
2244
  if (value.includes("\x00"))
2236
2245
  return true;
@@ -2704,15 +2713,6 @@ function parseAnnotateArgs(argv) {
2704
2713
  }
2705
2714
  return { ok: false, error: `unknown annotate command: ${subcommand}` };
2706
2715
  }
2707
- async function readStdin() {
2708
- if (process.stdin.isTTY)
2709
- return "";
2710
- const chunks = [];
2711
- for await (const chunk of process.stdin) {
2712
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2713
- }
2714
- return Buffer.concat(chunks).toString("utf8");
2715
- }
2716
2716
  async function annotateRequest(serverUrl, method, action, body) {
2717
2717
  return requestJson(serverUrl, "/_annotations", method, body, action);
2718
2718
  }
@@ -3016,8 +3016,8 @@ location and renders your explanation directly under the annotated lines.
3016
3016
  - The body is Markdown. Code spans, fenced blocks, and links work. Long
3017
3017
  bodies: use --body-file <path> or pipe via stdin instead of --body.
3018
3018
  - Give every annotation a short --title; it becomes the inline heading.
3019
- - Annotating unchanged code is fine: the viewer auto-expands diff context
3020
- or falls back to the full source view.
3019
+ - Annotating unchanged code is fine: when the selected lines are not an
3020
+ after-side diff change, the viewer falls back to the full source view.
3021
3021
 
3022
3022
  ## Sessions
3023
3023
 
@@ -3080,7 +3080,7 @@ bunch of probing commands or open the \uD83E\uDE7A browser panel.
3080
3080
  ## When to use
3081
3081
 
3082
3082
  - A previous command failed; you want to confirm Node / Bun / SQLite /
3083
- Docker / git state before suggesting a fix.
3083
+ Docker / git / gh state before suggesting a fix.
3084
3084
  - The human asks "is my environment OK", "why does install fail", or
3085
3085
  similar; \`code-viewer doctor\` is the single source of truth.
3086
3086
  - Pre-flight before a destructive step (DB snapshot reset, npx upgrade)
@@ -3111,7 +3111,7 @@ Stable JSON contract (see core/doctor-types.ts):
3111
3111
  "generation": number, // monotonic, restart resets to 1
3112
3112
  "worstStatus": "ok"|"warn"|"error",
3113
3113
  "groups": [
3114
- { "id": "runtime"|"package"|"sqlite"|"snapshot"|"git"
3114
+ { "id": "runtime"|"package"|"sqlite"|"snapshot"|"git"|"github"
3115
3115
  |"discovery"|"datastore"|"docker"|"server",
3116
3116
  "title": string,
3117
3117
  "rows": [
@@ -3142,6 +3142,9 @@ The exit code is 1 iff \`worstStatus === "error"\` — never on \`warn\`.
3142
3142
  - \`hint\` is the human-readable fix; quote it verbatim when reporting back.
3143
3143
  - A \`runtime.node\` row failing means Node < 20 — almost everything else
3144
3144
  is downstream of that. Fix it first.
3145
+ - A \`github.gh\` row warning means GitHub Issue listing/linking cannot use
3146
+ the GitHub CLI yet. Install \`gh\` or pass \`--bin gh=/absolute/path\`.
3147
+ Doctor intentionally does not inspect GitHub auth metadata.
3145
3148
  - A \`sqlite.*\` row failing usually points at npx cache; the hint shows
3146
3149
  the rm -rf ~/.npm/_npx workaround.
3147
3150
  - Docker rows are advisory — \`code-viewer\` works without Docker; warnings
@@ -4045,123 +4048,1291 @@ Diff Viewer views.
4045
4048
  - Git must be available on PATH, or supplied with --bin git=/absolute/path
4046
4049
  / CODE_VIEWER_BIN_GIT.
4047
4050
 
4048
- ## How to call
4051
+ ## How to call
4052
+
4053
+ code-viewer file blame --path src/sample.ts --json
4054
+ code-viewer file blame --path src/sample.ts --base HEAD --json
4055
+ code-viewer file history --path src/sample.ts --limit 10 --json
4056
+ code-viewer file history --path src/sample.ts --query "author:tester" --json
4057
+ code-viewer file show --path src/sample.ts --json
4058
+ code-viewer file show --path src/sample.ts --start 100 --end 150 --json
4059
+ code-viewer file show --path src/sample.ts --ref main --json
4060
+ code-viewer file diff --path src/sample.ts --json
4061
+ code-viewer file diff --path src/sample.ts --from main --to HEAD --json
4062
+ code-viewer file diff --path src/sample.ts --full --json
4063
+ code-viewer file diff --path new_sample.ts --untracked --json
4064
+ code-viewer file diff --path renamed.ts --old-path original.ts --from HEAD~1 --to HEAD --json
4065
+
4066
+ ## Output contract
4067
+
4068
+ --json shapes (mirroring git.ts DTOs verbatim where possible):
4069
+
4070
+ blame:
4071
+ {
4072
+ "path": string,
4073
+ "ref": string, // the literal ref the CLI resolved to
4074
+ "base": "worktree"|"HEAD",
4075
+ "result": GitBlameResult // { lines, commits, isUntracked?, isSynthetic?, error? }
4076
+ }
4077
+ history:
4078
+ {
4079
+ "path": string,
4080
+ "ref": string,
4081
+ "limit": number,
4082
+ "skip": number,
4083
+ "query"?: string,
4084
+ "result": { commits: GitHistoryCommit[], hasMore: boolean, error?: string }
4085
+ }
4086
+ show:
4087
+ {
4088
+ "path": string,
4089
+ "ref": string,
4090
+ "start"?: number,
4091
+ "end"?: number,
4092
+ "totalLines": number, // total lines in the file at <ref>
4093
+ "complete": boolean, // true when the returned text covers the whole file
4094
+ "text": string
4095
+ }
4096
+ diff:
4097
+ {
4098
+ "path": string,
4099
+ "old_path"?: string,
4100
+ "from": string, // "/dev/null" when untracked is true
4101
+ "to": string,
4102
+ "untracked": boolean,
4103
+ "ignore_ws": boolean,
4104
+ "ignore_blank": boolean,
4105
+ "mode": "preview"|"full",
4106
+ "max_hunks": number|null, // null in full mode
4107
+ "max_lines": number|null, // null in full mode
4108
+ "diff": string, // unified diff text
4109
+ "hunk_count": number, // total hunks before truncation
4110
+ "rendered_hunk_count": number, // hunks actually included in diff
4111
+ "line_count": number, // total newlines in the returned diff
4112
+ "truncated": boolean, // true when preview dropped hunks/lines
4113
+ "binary": boolean, // git emitted "Binary files ..."
4114
+ "error"?: string // git stderr (also exits 1)
4115
+ }
4116
+
4117
+ Default (non --json) output:
4118
+
4119
+ blame: one line per source line → <line><TAB><shortSha or "worktree"><TAB><summary>
4120
+ uncommitted edits show as "worktree" and "<uncommitted>".
4121
+ history: one line per commit → <shortSha><TAB><whenISO><TAB><author><TAB><subject>
4122
+ 0 commits prints "no history" to stderr, exit 0.
4123
+ show: the file (or sliced lines) as text. 0-byte files succeed (exit 0).
4124
+ diff: unified diff text (the same bytes the JSON "diff" field carries).
4125
+ Empty (or worktree==worktree) returns nothing on stdout, exit 0.
4126
+
4127
+ ## Tips
4128
+
4129
+ - Prefer --json. blame DTO carries author / authorMail / authorTime per
4130
+ commit; the plain-text format only shows summary.
4131
+ - For huge files, slice with --start/--end before piping to a model
4132
+ context — "totalLines" / "complete" tell you what was dropped.
4133
+ - file show reads the worktree by default. Pass --ref HEAD / --ref <branch>
4134
+ when you need a committed snapshot.
4135
+ - A non-fatal git failure (e.g. unknown ref, unsafe path) returns the
4136
+ error string inside the JSON "result.error" / "error" field AND exits 1.
4137
+ - file diff defaults to preview mode (${FILE_DIFF_DEFAULT_MAX_HUNKS} hunks /
4138
+ ${FILE_DIFF_DEFAULT_MAX_LINES} lines). Use --full only when you need every
4139
+ hunk — preview matches the browser's initial paint and is almost always
4140
+ enough context for an LLM. In full mode, max_hunks / max_lines are null in
4141
+ JSON because no preview cap is applied. Worktree == Worktree range returns
4142
+ "" instantly.
4143
+ - Use "code-viewer search code" to discover the path first, then drill
4144
+ in with blame / history / show / diff.
4145
+ `;
4146
+ VALUE_FLAGS = new Set([
4147
+ "--path",
4148
+ "--ref",
4149
+ "--base",
4150
+ "--limit",
4151
+ "--skip",
4152
+ "--query",
4153
+ "--start",
4154
+ "--end",
4155
+ "--from",
4156
+ "--to",
4157
+ "--old-path",
4158
+ "--max-hunks",
4159
+ "--max-lines"
4160
+ ]);
4161
+ BOOL_FLAGS = new Set([
4162
+ "--json",
4163
+ "--untracked",
4164
+ "--ignore-ws",
4165
+ "--ignore-blank",
4166
+ "--full"
4167
+ ]);
4168
+ });
4169
+
4170
+ // web-src/core/journal.ts
4171
+ function isJournalTaskStatus(value) {
4172
+ return JOURNAL_TASK_STATUSES.includes(value);
4173
+ }
4174
+ function isJournalTaskPriority(value) {
4175
+ return JOURNAL_TASK_PRIORITIES.includes(value);
4176
+ }
4177
+ function isIsoDate(value) {
4178
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value))
4179
+ return false;
4180
+ const date = new Date(`${value}T00:00:00.000Z`);
4181
+ return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
4182
+ }
4183
+ function todayIsoDate(now = new Date) {
4184
+ const year = now.getFullYear();
4185
+ const month = String(now.getMonth() + 1).padStart(2, "0");
4186
+ const date = String(now.getDate()).padStart(2, "0");
4187
+ return `${year}-${month}-${date}`;
4188
+ }
4189
+ function taskClaimActive(task, now = Date.now()) {
4190
+ if (!task.claim)
4191
+ return false;
4192
+ const expires = Date.parse(task.claim.lease_expires_at);
4193
+ return Number.isFinite(expires) && expires > now;
4194
+ }
4195
+ function normalizeJournalLabel(value) {
4196
+ if (typeof value !== "string")
4197
+ return null;
4198
+ const label = value.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^\p{L}\p{N}._:-]+/gu, "").slice(0, JOURNAL_LABEL_MAX_CHARS).replace(/^[-_.:]+|[-_.:]+$/g, "");
4199
+ return label || null;
4200
+ }
4201
+ function normalizeJournalLabels(raw) {
4202
+ if (!Array.isArray(raw))
4203
+ return [];
4204
+ const labels = [];
4205
+ const seen = new Set;
4206
+ for (const item of raw) {
4207
+ const label = normalizeJournalLabel(item);
4208
+ if (!label || seen.has(label))
4209
+ continue;
4210
+ seen.add(label);
4211
+ labels.push(label);
4212
+ if (labels.length >= JOURNAL_MAX_LABELS)
4213
+ break;
4214
+ }
4215
+ return labels;
4216
+ }
4217
+ function journalIssueLabel(issueNumber) {
4218
+ return `issue-${issueNumber}`;
4219
+ }
4220
+ function journalIssueRepoLabel(repo) {
4221
+ if (typeof repo !== "string")
4222
+ return null;
4223
+ return normalizeJournalLabel(`repo-${repo.replace(/[\\/]+/g, "-")}`);
4224
+ }
4225
+ function collectJournalLabels(journal, tasks) {
4226
+ const labels = new Set;
4227
+ for (const entry of journal.entries) {
4228
+ for (const label of entry.labels)
4229
+ labels.add(label);
4230
+ }
4231
+ for (const task of tasks.tasks) {
4232
+ for (const label of task.labels)
4233
+ labels.add(label);
4234
+ }
4235
+ return [...labels].sort((a, b) => a.localeCompare(b));
4236
+ }
4237
+ function filterJournalTasks(state, filter = {}) {
4238
+ const statuses = Array.isArray(filter.status) ? filter.status : filter.status ? [filter.status] : undefined;
4239
+ const labels = filter.labels?.filter(Boolean) || [];
4240
+ return state.tasks.filter((task) => {
4241
+ if (statuses && !statuses.includes(task.status))
4242
+ return false;
4243
+ if (labels.length && !labels.every((label) => task.labels.includes(label))) {
4244
+ return false;
4245
+ }
4246
+ if (!filter.includeClaimed && taskClaimActive(task, filter.now))
4247
+ return false;
4248
+ return true;
4249
+ });
4250
+ }
4251
+ function selectNextJournalTasks(state, filter = {}, limit = 1) {
4252
+ const status = filter.status || "todo";
4253
+ const indexed = filterJournalTasks(state, { ...filter, status }).map((task, index) => ({ task, index }));
4254
+ indexed.sort((a, b) => {
4255
+ const priority = PRIORITY_SCORE[a.task.priority] - PRIORITY_SCORE[b.task.priority];
4256
+ if (priority !== 0)
4257
+ return priority;
4258
+ if (a.index !== b.index)
4259
+ return a.index - b.index;
4260
+ return a.task.created_at.localeCompare(b.task.created_at);
4261
+ });
4262
+ return indexed.slice(0, Math.max(0, limit)).map((item) => item.task);
4263
+ }
4264
+ var JOURNAL_TASK_STATUSES, JOURNAL_TASK_PRIORITIES, JOURNAL_LABEL_MAX_CHARS = 48, JOURNAL_MAX_LABELS = 24, PRIORITY_SCORE;
4265
+ var init_journal = __esm(() => {
4266
+ JOURNAL_TASK_STATUSES = [
4267
+ "draft",
4268
+ "todo",
4269
+ "doing",
4270
+ "blocked",
4271
+ "done"
4272
+ ];
4273
+ JOURNAL_TASK_PRIORITIES = ["p0", "p1", "p2", "p3"];
4274
+ PRIORITY_SCORE = {
4275
+ p0: 0,
4276
+ p1: 1,
4277
+ p2: 2,
4278
+ p3: 3
4279
+ };
4280
+ });
4281
+
4282
+ // web-src/server/github-issues.ts
4283
+ function normalizeGithubIssueListState(value) {
4284
+ return value === "closed" || value === "all" ? value : "open";
4285
+ }
4286
+ function normalizeGithubIssueListLimit(value) {
4287
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 1) {
4288
+ return 30;
4289
+ }
4290
+ return Math.min(Math.floor(value), 100);
4291
+ }
4292
+ function singleLineGithubOption(value) {
4293
+ if (!value)
4294
+ return;
4295
+ const trimmed = value.trim();
4296
+ if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed)) {
4297
+ return;
4298
+ }
4299
+ return trimmed.slice(0, 200);
4300
+ }
4301
+ function githubSearchHasStateQualifier(value) {
4302
+ const search = singleLineGithubOption(value);
4303
+ return !!search && /(^|\s)(is|state):(open|closed|all)(?=\s|$)/i.test(search);
4304
+ }
4305
+ function extractGithubIssueLabels(raw) {
4306
+ if (!Array.isArray(raw))
4307
+ return [];
4308
+ const labels = [];
4309
+ for (const item of raw) {
4310
+ if (typeof item === "string") {
4311
+ const label = singleLineGithubOption(item);
4312
+ if (label)
4313
+ labels.push(label);
4314
+ } else if (item && typeof item === "object" && "name" in item) {
4315
+ const label = singleLineGithubOption(item.name);
4316
+ if (label)
4317
+ labels.push(label);
4318
+ }
4319
+ }
4320
+ return labels.slice(0, 12);
4321
+ }
4322
+ function normalizeGithubIssueListItem(raw) {
4323
+ if (!raw || typeof raw !== "object")
4324
+ return null;
4325
+ const issue = raw;
4326
+ const number = issue.number;
4327
+ const title = issue.title;
4328
+ if (typeof number !== "number" || !Number.isInteger(number) || number <= 0 || typeof title !== "string" || !title.trim()) {
4329
+ return null;
4330
+ }
4331
+ const url = singleLineGithubOption(issue.url);
4332
+ const state = typeof issue.state === "string" && issue.state.trim() ? issue.state.trim().toLowerCase() : "open";
4333
+ return {
4334
+ number,
4335
+ title: title.trim().slice(0, 200),
4336
+ state,
4337
+ ...url ? { url } : {},
4338
+ labels: extractGithubIssueLabels(issue.labels)
4339
+ };
4340
+ }
4341
+ function parseGithubIssueListOutput(stdout) {
4342
+ const parsed = JSON.parse(stdout);
4343
+ if (!Array.isArray(parsed))
4344
+ return [];
4345
+ return parsed.map(normalizeGithubIssueListItem).filter((issue) => issue !== null);
4346
+ }
4347
+ function parseGithubIssueViewOutput(stdout) {
4348
+ const issue = normalizeGithubIssueListItem(JSON.parse(stdout));
4349
+ if (!issue)
4350
+ throw new GithubIssueListError("failed to parse gh issue output");
4351
+ return issue;
4352
+ }
4353
+ function buildGithubIssueListArgs(options) {
4354
+ const search = singleLineGithubOption(options.search);
4355
+ const args = [
4356
+ commandForExternal("gh"),
4357
+ "issue",
4358
+ "list",
4359
+ "--json",
4360
+ "number,title,state,labels,url",
4361
+ "--limit",
4362
+ String(normalizeGithubIssueListLimit(options.limit)),
4363
+ "--state",
4364
+ search && githubSearchHasStateQualifier(search) ? "all" : normalizeGithubIssueListState(options.state)
4365
+ ];
4366
+ const repo = singleLineGithubOption(options.repo);
4367
+ if (repo)
4368
+ args.push("--repo", repo);
4369
+ if (search)
4370
+ args.push("--search", search);
4371
+ const seenLabels = new Set;
4372
+ for (const rawLabel of options.labels || []) {
4373
+ const label = singleLineGithubOption(rawLabel);
4374
+ if (!label || seenLabels.has(label))
4375
+ continue;
4376
+ seenLabels.add(label);
4377
+ args.push("--label", label);
4378
+ if (seenLabels.size >= GITHUB_ISSUE_LABEL_FILTER_LIMIT)
4379
+ break;
4380
+ }
4381
+ return args;
4382
+ }
4383
+ function buildGithubIssueViewArgs(options) {
4384
+ const args = [
4385
+ commandForExternal("gh"),
4386
+ "issue",
4387
+ "view",
4388
+ String(options.number),
4389
+ "--json",
4390
+ "number,title,state,labels,url"
4391
+ ];
4392
+ const repo = singleLineGithubOption(options.repo);
4393
+ if (repo)
4394
+ args.push("--repo", repo);
4395
+ return args;
4396
+ }
4397
+ function readGithubIssueList(options) {
4398
+ const proc = runSync(buildGithubIssueListArgs(options), options.cwd, {
4399
+ timeout: 30000
4400
+ });
4401
+ if (proc.code !== 0) {
4402
+ const detail = isCommandNotFoundResult("gh", proc) ? commandNotFoundDetail("gh") : proc.stderr.trim() || `gh issue list exited with code ${proc.code}`;
4403
+ throw new GithubIssueListError(detail);
4404
+ }
4405
+ try {
4406
+ return parseGithubIssueListOutput(proc.stdout);
4407
+ } catch {
4408
+ throw new GithubIssueListError("failed to parse gh issue list output");
4409
+ }
4410
+ }
4411
+ function readGithubIssue(options) {
4412
+ const proc = runSync(buildGithubIssueViewArgs(options), options.cwd, {
4413
+ timeout: 30000
4414
+ });
4415
+ if (proc.code !== 0) {
4416
+ const detail = isCommandNotFoundResult("gh", proc) ? commandNotFoundDetail("gh") : proc.stderr.trim() || `gh issue view exited with code ${proc.code}`;
4417
+ throw new GithubIssueListError(detail);
4418
+ }
4419
+ try {
4420
+ return parseGithubIssueViewOutput(proc.stdout);
4421
+ } catch (error) {
4422
+ if (error instanceof GithubIssueListError)
4423
+ throw error;
4424
+ throw new GithubIssueListError("failed to parse gh issue output");
4425
+ }
4426
+ }
4427
+ var GITHUB_ISSUE_LABEL_FILTER_LIMIT = 24, GithubIssueListError;
4428
+ var init_github_issues = __esm(() => {
4429
+ init_command_resolver();
4430
+ init_runtime();
4431
+ GithubIssueListError = class GithubIssueListError extends Error {
4432
+ status;
4433
+ constructor(message, status = 502) {
4434
+ super(message);
4435
+ this.status = status;
4436
+ }
4437
+ };
4438
+ });
4439
+
4440
+ // web-src/server/journal-cli.ts
4441
+ var exports_journal_cli = {};
4442
+ __export(exports_journal_cli, {
4443
+ runJournalCli: () => runJournalCli,
4444
+ parseJournalArgs: () => parseJournalArgs,
4445
+ JOURNAL_HELP: () => JOURNAL_HELP,
4446
+ JOURNAL_AGENT_HELP: () => JOURNAL_AGENT_HELP
4447
+ });
4448
+ import { readFileSync as readFileSync5 } from "node:fs";
4449
+ function parsePositiveInteger(value, flag) {
4450
+ if (value === undefined)
4451
+ return;
4452
+ const n = Number(value);
4453
+ if (!Number.isInteger(n) || n < 1)
4454
+ throw new Error(`${flag} must be a positive integer`);
4455
+ return n;
4456
+ }
4457
+ function parseDateValue(value, flag) {
4458
+ if (value === undefined)
4459
+ return;
4460
+ if (value === "today")
4461
+ return todayIsoDate();
4462
+ if (value === "none")
4463
+ return "";
4464
+ if (!isIsoDate(value))
4465
+ throw new Error(`${flag} must be YYYY-MM-DD or today`);
4466
+ return value;
4467
+ }
4468
+ function parseStatus(value) {
4469
+ if (value === undefined)
4470
+ return;
4471
+ if (isJournalTaskStatus(value))
4472
+ return value;
4473
+ throw new Error("--status must be draft, todo, doing, blocked, or done");
4474
+ }
4475
+ function parsePriority(value) {
4476
+ if (value === undefined)
4477
+ return;
4478
+ if (isJournalTaskPriority(value))
4479
+ return value;
4480
+ throw new Error("--priority must be p0, p1, p2, or p3");
4481
+ }
4482
+ function parseGithubIssueState(value) {
4483
+ if (value === undefined)
4484
+ return "open";
4485
+ if (value === "open" || value === "closed" || value === "all")
4486
+ return value;
4487
+ throw new Error("--state must be open, closed, or all");
4488
+ }
4489
+ function parseGithubOption(value, flag) {
4490
+ if (value === undefined)
4491
+ return;
4492
+ const normalized = singleLineGithubOption(value);
4493
+ if (!normalized)
4494
+ throw new Error(`${flag} must be a non-empty single line`);
4495
+ return normalized;
4496
+ }
4497
+ function parseJournalArgs(argv) {
4498
+ const rest = [];
4499
+ let cwd;
4500
+ let server;
4501
+ const options = new Map;
4502
+ const multiOptions = new Map;
4503
+ const flags = new Set;
4504
+ const commandOverrides = [];
4505
+ const valueFlags = new Set([
4506
+ "--date",
4507
+ "--title",
4508
+ "--body",
4509
+ "--body-file",
4510
+ "--note",
4511
+ "--note-file",
4512
+ "--label",
4513
+ "--status",
4514
+ "--priority",
4515
+ "--due",
4516
+ "--source-date",
4517
+ "--before",
4518
+ "--after",
4519
+ "--position",
4520
+ "--by",
4521
+ "--lease-minutes",
4522
+ "--wip-limit",
4523
+ "--limit",
4524
+ "--repo",
4525
+ "--gh-label",
4526
+ "--search",
4527
+ "--state"
4528
+ ]);
4529
+ try {
4530
+ for (let i = 0;i < argv.length; i++) {
4531
+ const arg = argv[i];
4532
+ if (arg === "--help" || arg === "-h")
4533
+ return { ok: true, args: { command: { kind: "help" }, dryRun: false } };
4534
+ if (arg === "--cwd" || arg === "--server") {
4535
+ const taken = takeValue(argv, i, arg);
4536
+ if ("error" in taken)
4537
+ return { ok: false, error: taken.error };
4538
+ if (arg === "--cwd")
4539
+ cwd = taken.value;
4540
+ else
4541
+ server = taken.value;
4542
+ i = taken.next;
4543
+ } else if (arg === "--bin") {
4544
+ const taken = takeValue(argv, i, arg);
4545
+ if ("error" in taken)
4546
+ return { ok: false, error: taken.error };
4547
+ const parsed = parseExternalCommandOverride(taken.value, "--bin", [
4548
+ "gh"
4549
+ ]);
4550
+ if (parsed.ok === false)
4551
+ return { ok: false, error: parsed.error };
4552
+ commandOverrides.push(parsed.override);
4553
+ i = taken.next;
4554
+ } else if (valueFlags.has(arg)) {
4555
+ const taken = takeValue(argv, i, arg);
4556
+ if ("error" in taken)
4557
+ return { ok: false, error: taken.error };
4558
+ options.set(arg, taken.value);
4559
+ const values = multiOptions.get(arg) || [];
4560
+ values.push(taken.value);
4561
+ multiOptions.set(arg, values);
4562
+ i = taken.next;
4563
+ } else if (arg === "--json" || arg === "--dry-run" || arg === "--clear-labels") {
4564
+ flags.add(arg);
4565
+ } else if (arg.startsWith("-")) {
4566
+ return { ok: false, error: `unknown option: ${arg}` };
4567
+ } else {
4568
+ rest.push(arg);
4569
+ }
4570
+ }
4571
+ const labels = multiOptions.get("--label") || [];
4572
+ const subcommand = rest[0];
4573
+ const dryRun = flags.has("--dry-run");
4574
+ if (!subcommand)
4575
+ return { ok: true, args: { command: { kind: "help" }, dryRun } };
4576
+ if (subcommand === "agent-help")
4577
+ return { ok: true, args: { command: { kind: "agent-help" }, dryRun } };
4578
+ if (commandOverrides.length > 0 && subcommand !== "github-issues" && subcommand !== "task-link-issue") {
4579
+ return {
4580
+ ok: false,
4581
+ error: "--bin is only supported with github-issues or task-link-issue"
4582
+ };
4583
+ }
4584
+ if (subcommand === "list") {
4585
+ return {
4586
+ ok: true,
4587
+ args: {
4588
+ command: {
4589
+ kind: "list",
4590
+ date: parseDateValue(options.get("--date"), "--date"),
4591
+ limit: parsePositiveInteger(options.get("--limit"), "--limit"),
4592
+ json: flags.has("--json")
4593
+ },
4594
+ cwd,
4595
+ server,
4596
+ dryRun
4597
+ }
4598
+ };
4599
+ }
4600
+ if (subcommand === "add") {
4601
+ const date = parseDateValue(options.get("--date"), "--date");
4602
+ if (!date)
4603
+ return { ok: false, error: "add requires --date <YYYY-MM-DD|today>" };
4604
+ return {
4605
+ ok: true,
4606
+ args: {
4607
+ command: {
4608
+ kind: "add",
4609
+ date,
4610
+ title: options.get("--title"),
4611
+ labels,
4612
+ body: options.get("--body"),
4613
+ bodyFile: options.get("--body-file")
4614
+ },
4615
+ cwd,
4616
+ server,
4617
+ dryRun
4618
+ }
4619
+ };
4620
+ }
4621
+ if (subcommand === "edit") {
4622
+ const id = rest[1];
4623
+ if (!id)
4624
+ return { ok: false, error: "edit requires a journal entry id" };
4625
+ return {
4626
+ ok: true,
4627
+ args: {
4628
+ command: {
4629
+ kind: "edit",
4630
+ id,
4631
+ date: parseDateValue(options.get("--date"), "--date"),
4632
+ title: options.get("--title"),
4633
+ labels: multiOptions.has("--label") ? labels : undefined,
4634
+ body: options.get("--body"),
4635
+ bodyFile: options.get("--body-file")
4636
+ },
4637
+ cwd,
4638
+ server,
4639
+ dryRun
4640
+ }
4641
+ };
4642
+ }
4643
+ if (subcommand === "tasks") {
4644
+ return {
4645
+ ok: true,
4646
+ args: {
4647
+ command: {
4648
+ kind: "tasks",
4649
+ status: parseStatus(options.get("--status")),
4650
+ labels,
4651
+ json: flags.has("--json")
4652
+ },
4653
+ cwd,
4654
+ server,
4655
+ dryRun
4656
+ }
4657
+ };
4658
+ }
4659
+ if (subcommand === "task-add") {
4660
+ const title = options.get("--title");
4661
+ if (!title)
4662
+ return { ok: false, error: "task-add requires --title <text>" };
4663
+ return {
4664
+ ok: true,
4665
+ args: {
4666
+ command: {
4667
+ kind: "task-add",
4668
+ title,
4669
+ status: parseStatus(options.get("--status")),
4670
+ priority: parsePriority(options.get("--priority")),
4671
+ labels,
4672
+ dueDate: parseDateValue(options.get("--due"), "--due"),
4673
+ sourceDate: parseDateValue(options.get("--source-date"), "--source-date"),
4674
+ before: options.get("--before"),
4675
+ after: options.get("--after"),
4676
+ position: parsePositiveInteger(options.get("--position"), "--position"),
4677
+ body: options.get("--body"),
4678
+ bodyFile: options.get("--body-file")
4679
+ },
4680
+ cwd,
4681
+ server,
4682
+ dryRun
4683
+ }
4684
+ };
4685
+ }
4686
+ if (subcommand === "task-update") {
4687
+ const id = rest[1];
4688
+ if (!id)
4689
+ return { ok: false, error: "task-update requires a task id" };
4690
+ return {
4691
+ ok: true,
4692
+ args: {
4693
+ command: {
4694
+ kind: "task-update",
4695
+ id,
4696
+ title: options.get("--title"),
4697
+ status: parseStatus(options.get("--status")),
4698
+ priority: parsePriority(options.get("--priority")),
4699
+ labels: multiOptions.has("--label") ? labels : undefined,
4700
+ clearLabels: flags.has("--clear-labels"),
4701
+ dueDate: parseDateValue(options.get("--due"), "--due") ?? undefined,
4702
+ sourceDate: parseDateValue(options.get("--source-date"), "--source-date") ?? undefined,
4703
+ body: options.get("--body"),
4704
+ bodyFile: options.get("--body-file")
4705
+ },
4706
+ cwd,
4707
+ server,
4708
+ dryRun
4709
+ }
4710
+ };
4711
+ }
4712
+ if (subcommand === "task-next") {
4713
+ return {
4714
+ ok: true,
4715
+ args: {
4716
+ command: {
4717
+ kind: "task-next",
4718
+ status: parseStatus(options.get("--status")),
4719
+ labels,
4720
+ limit: parsePositiveInteger(options.get("--limit"), "--limit") || 1,
4721
+ json: flags.has("--json")
4722
+ },
4723
+ cwd,
4724
+ server,
4725
+ dryRun
4726
+ }
4727
+ };
4728
+ }
4729
+ if (subcommand === "github-issues") {
4730
+ if (multiOptions.has("--label")) {
4731
+ return {
4732
+ ok: false,
4733
+ error: "github-issues uses --gh-label, not --label"
4734
+ };
4735
+ }
4736
+ return {
4737
+ ok: true,
4738
+ args: {
4739
+ command: {
4740
+ kind: "github-issues",
4741
+ repo: parseGithubOption(options.get("--repo"), "--repo"),
4742
+ ghState: parseGithubIssueState(options.get("--state")),
4743
+ ghLabels: (multiOptions.get("--gh-label") || []).map((label) => parseGithubOption(label, "--gh-label")),
4744
+ search: parseGithubOption(options.get("--search"), "--search"),
4745
+ limit: parsePositiveInteger(options.get("--limit"), "--limit") || 30,
4746
+ json: flags.has("--json")
4747
+ },
4748
+ cwd,
4749
+ server,
4750
+ commandOverrides,
4751
+ dryRun
4752
+ }
4753
+ };
4754
+ }
4755
+ if (subcommand === "task-link-issue") {
4756
+ if (multiOptions.has("--gh-label")) {
4757
+ return {
4758
+ ok: false,
4759
+ error: "task-link-issue uses --label for local labels"
4760
+ };
4761
+ }
4762
+ const issueNumber = parsePositiveInteger(rest[1], "issue number");
4763
+ if (!issueNumber) {
4764
+ return {
4765
+ ok: false,
4766
+ error: "task-link-issue requires <number>"
4767
+ };
4768
+ }
4769
+ return {
4770
+ ok: true,
4771
+ args: {
4772
+ command: {
4773
+ kind: "task-link-issue",
4774
+ issueNumber,
4775
+ repo: parseGithubOption(options.get("--repo"), "--repo"),
4776
+ status: parseStatus(options.get("--status")),
4777
+ priority: parsePriority(options.get("--priority")),
4778
+ labels,
4779
+ before: options.get("--before"),
4780
+ after: options.get("--after"),
4781
+ position: parsePositiveInteger(options.get("--position"), "--position"),
4782
+ json: flags.has("--json")
4783
+ },
4784
+ cwd,
4785
+ server,
4786
+ commandOverrides,
4787
+ dryRun
4788
+ }
4789
+ };
4790
+ }
4791
+ if (subcommand === "task-claim") {
4792
+ const id = rest[1];
4793
+ if (!id)
4794
+ return { ok: false, error: "task-claim requires a task id" };
4795
+ return {
4796
+ ok: true,
4797
+ args: {
4798
+ command: {
4799
+ kind: "task-claim",
4800
+ id,
4801
+ by: options.get("--by"),
4802
+ leaseMinutes: parsePositiveInteger(options.get("--lease-minutes"), "--lease-minutes"),
4803
+ wipLimit: parsePositiveInteger(options.get("--wip-limit"), "--wip-limit")
4804
+ },
4805
+ cwd,
4806
+ server,
4807
+ dryRun
4808
+ }
4809
+ };
4810
+ }
4811
+ if (subcommand === "task-done") {
4812
+ const id = rest[1];
4813
+ if (!id)
4814
+ return { ok: false, error: "task-done requires a task id" };
4815
+ if (!options.get("--by"))
4816
+ return { ok: false, error: "task-done requires --by <agent>" };
4817
+ return {
4818
+ ok: true,
4819
+ args: {
4820
+ command: {
4821
+ kind: "task-done",
4822
+ id,
4823
+ by: options.get("--by"),
4824
+ note: options.get("--note"),
4825
+ noteFile: options.get("--note-file")
4826
+ },
4827
+ cwd,
4828
+ server,
4829
+ dryRun
4830
+ }
4831
+ };
4832
+ }
4833
+ if (subcommand === "task-delete") {
4834
+ const id = rest[1];
4835
+ if (!id)
4836
+ return { ok: false, error: "task-delete requires a task id" };
4837
+ return {
4838
+ ok: true,
4839
+ args: { command: { kind: "task-delete", id }, cwd, server, dryRun }
4840
+ };
4841
+ }
4842
+ return { ok: false, error: `unknown journal command: ${subcommand}` };
4843
+ } catch (error) {
4844
+ return {
4845
+ ok: false,
4846
+ error: error instanceof Error ? error.message : "invalid journal arguments"
4847
+ };
4848
+ }
4849
+ }
4850
+ async function textFromBody(command, required) {
4851
+ if (command.body !== undefined && command.bodyFile !== undefined) {
4852
+ console.error("use either --body or --body-file");
4853
+ process.exit(1);
4854
+ }
4855
+ if (command.body !== undefined)
4856
+ return command.body;
4857
+ if (command.bodyFile !== undefined) {
4858
+ try {
4859
+ return readFileSync5(command.bodyFile, "utf8");
4860
+ } catch {
4861
+ console.error(`could not read --body-file: ${command.bodyFile}`);
4862
+ process.exit(1);
4863
+ }
4864
+ }
4865
+ const stdin = await readStdin();
4866
+ if (stdin.trim())
4867
+ return stdin;
4868
+ if (required) {
4869
+ console.error("body is empty. Pass --body, --body-file, or pipe stdin.");
4870
+ process.exit(1);
4871
+ }
4872
+ return;
4873
+ }
4874
+ function textFromNote(command) {
4875
+ if (command.note !== undefined && command.noteFile !== undefined) {
4876
+ console.error("use either --note or --note-file");
4877
+ process.exit(1);
4878
+ }
4879
+ if (command.note !== undefined)
4880
+ return command.note;
4881
+ if (command.noteFile !== undefined) {
4882
+ try {
4883
+ return readFileSync5(command.noteFile, "utf8");
4884
+ } catch {
4885
+ console.error(`could not read --note-file: ${command.noteFile}`);
4886
+ process.exit(1);
4887
+ }
4888
+ }
4889
+ return;
4890
+ }
4891
+ async function journalRequest(serverUrl, method, action, body) {
4892
+ return requestJson(serverUrl, "/_journal", method, body, action);
4893
+ }
4894
+ function selectEntries(data, date, limit) {
4895
+ const entries = date ? data.journal.entries.filter((entry) => entry.date === date) : data.journal.entries;
4896
+ if (limit === undefined)
4897
+ return entries;
4898
+ return [...entries].sort((a, b) => b.date.localeCompare(a.date) || b.updated_at.localeCompare(a.updated_at) || b.created_at.localeCompare(a.created_at)).slice(0, limit);
4899
+ }
4900
+ function printEntries(data, date, limit) {
4901
+ const entries = selectEntries(data, date, limit);
4902
+ if (!entries.length) {
4903
+ console.log("no journal entries");
4904
+ return;
4905
+ }
4906
+ for (const entry of entries) {
4907
+ const labels = entry.labels.length ? ` #${entry.labels.join(" #")}` : "";
4908
+ const title = entry.title ? ` ${entry.title}` : "";
4909
+ console.log(`${entry.date} [${entry.id}]${title}${labels}`);
4910
+ console.log(` ${entry.body.split(`
4911
+ `)[0].slice(0, 120)}`);
4912
+ }
4913
+ }
4914
+ function printTasks(tasks) {
4915
+ if (!tasks.length) {
4916
+ console.log("no tasks");
4917
+ return;
4918
+ }
4919
+ for (const task of tasks) {
4920
+ const labels = task.labels.length ? ` #${task.labels.join(" #")}` : "";
4921
+ console.log(`[${task.id}] ${task.status} ${task.priority} ${task.title}${labels}`);
4922
+ }
4923
+ }
4924
+ function printGithubIssues(issues) {
4925
+ if (!issues.length) {
4926
+ console.log("no GitHub issues");
4927
+ return;
4928
+ }
4929
+ for (const issue of issues) {
4930
+ const labels = issue.labels.length ? ` #${issue.labels.join(" #")}` : "";
4931
+ const url = issue.url ? ` ${issue.url}` : "";
4932
+ console.log(`#${issue.number} ${issue.state} ${issue.title}${labels}${url}`);
4933
+ }
4934
+ }
4935
+ function taskLinkIssuePayload(command, issue) {
4936
+ return {
4937
+ action: "link-github-issue",
4938
+ issue_number: issue.number,
4939
+ repo: command.repo,
4940
+ title: issue.title,
4941
+ url: issue.url,
4942
+ memo_label: "Memo:",
4943
+ status: command.status,
4944
+ priority: command.priority,
4945
+ labels: command.labels,
4946
+ before_id: command.before,
4947
+ after_id: command.after,
4948
+ position: command.position
4949
+ };
4950
+ }
4951
+ function writePayload(payload) {
4952
+ console.log(JSON.stringify(payload, null, 2));
4953
+ }
4954
+ async function getJournalData(serverUrl) {
4955
+ return await journalRequest(serverUrl, "GET", "journal list");
4956
+ }
4957
+ async function runJournalCli(argv) {
4958
+ const parsed = parseJournalArgs(argv);
4959
+ if (parsed.ok === false) {
4960
+ console.error(parsed.error);
4961
+ console.error('Run "code-viewer journal --help" for usage.');
4962
+ process.exit(1);
4963
+ }
4964
+ const { command, cwd, server, commandOverrides = [], dryRun } = parsed.args;
4965
+ if (command.kind === "help") {
4966
+ console.log(JOURNAL_HELP);
4967
+ return;
4968
+ }
4969
+ if (command.kind === "agent-help") {
4970
+ console.log(JOURNAL_AGENT_HELP);
4971
+ return;
4972
+ }
4973
+ const dryRunPayload = async (action, body) => {
4974
+ if (!dryRun)
4975
+ return false;
4976
+ writePayload({ action, ...body });
4977
+ return true;
4978
+ };
4979
+ if (dryRun) {
4980
+ if (command.kind === "add") {
4981
+ writePayload({
4982
+ action: "add-entry",
4983
+ date: command.date,
4984
+ title: command.title,
4985
+ labels: command.labels,
4986
+ body: await textFromBody(command, true)
4987
+ });
4988
+ return;
4989
+ }
4990
+ if (command.kind === "edit") {
4991
+ writePayload({
4992
+ action: "update-entry",
4993
+ id: command.id,
4994
+ date: command.date,
4995
+ title: command.title,
4996
+ labels: command.labels,
4997
+ body: await textFromBody(command, false)
4998
+ });
4999
+ return;
5000
+ }
5001
+ if (command.kind === "task-add") {
5002
+ writePayload({
5003
+ action: "add-task",
5004
+ title: command.title,
5005
+ status: command.status,
5006
+ priority: command.priority,
5007
+ labels: command.labels,
5008
+ due_date: command.dueDate === "" ? null : command.dueDate,
5009
+ source_date: command.sourceDate === "" ? null : command.sourceDate,
5010
+ before_id: command.before,
5011
+ after_id: command.after,
5012
+ position: command.position,
5013
+ body: await textFromBody(command, false)
5014
+ });
5015
+ return;
5016
+ }
5017
+ if (command.kind === "task-update") {
5018
+ writePayload({
5019
+ action: "update-task",
5020
+ id: command.id,
5021
+ title: command.title,
5022
+ status: command.status,
5023
+ priority: command.priority,
5024
+ labels: command.clearLabels ? [] : command.labels,
5025
+ due_date: command.dueDate === "" ? null : command.dueDate,
5026
+ source_date: command.sourceDate === "" ? null : command.sourceDate,
5027
+ body: await textFromBody(command, false)
5028
+ });
5029
+ return;
5030
+ }
5031
+ if (command.kind === "task-claim") {
5032
+ writePayload({
5033
+ action: "claim-task",
5034
+ id: command.id,
5035
+ by: command.by,
5036
+ lease_minutes: command.leaseMinutes,
5037
+ wip_limit: command.wipLimit
5038
+ });
5039
+ return;
5040
+ }
5041
+ if (command.kind === "task-done") {
5042
+ writePayload({
5043
+ action: "complete-task",
5044
+ id: command.id,
5045
+ by: command.by,
5046
+ note: textFromNote(command)
5047
+ });
5048
+ return;
5049
+ }
5050
+ if (command.kind === "task-delete") {
5051
+ writePayload({ action: "delete-task", id: command.id });
5052
+ return;
5053
+ }
5054
+ }
5055
+ const root = resolveRepoRoot(cwd);
5056
+ if (command.kind === "github-issues") {
5057
+ const commandConfig = configureExternalCommands({
5058
+ cwd: root,
5059
+ cliOverrides: commandOverrides,
5060
+ allowedNames: ["gh"]
5061
+ });
5062
+ if (commandConfig.ok === false) {
5063
+ console.error(commandConfig.error);
5064
+ process.exit(1);
5065
+ }
5066
+ const issues = readGithubIssueList({
5067
+ cwd: root,
5068
+ repo: command.repo,
5069
+ labels: command.ghLabels,
5070
+ search: command.search,
5071
+ state: command.ghState,
5072
+ limit: command.limit
5073
+ });
5074
+ if (command.json)
5075
+ console.log(JSON.stringify({ issues }, null, 2));
5076
+ else
5077
+ printGithubIssues(issues);
5078
+ return;
5079
+ }
5080
+ if (command.kind === "task-link-issue") {
5081
+ const commandConfig = configureExternalCommands({
5082
+ cwd: root,
5083
+ cliOverrides: commandOverrides,
5084
+ allowedNames: ["gh"]
5085
+ });
5086
+ if (commandConfig.ok === false) {
5087
+ console.error(commandConfig.error);
5088
+ process.exit(1);
5089
+ }
5090
+ const issue = readGithubIssue({
5091
+ cwd: root,
5092
+ number: command.issueNumber,
5093
+ repo: command.repo
5094
+ });
5095
+ if (dryRun) {
5096
+ writePayload(taskLinkIssuePayload(command, issue));
5097
+ return;
5098
+ }
5099
+ const serverUrl2 = await ensureServerUrl(root, server, "/_journal");
5100
+ const result2 = await journalRequest(serverUrl2, "POST", "journal task-link-issue", taskLinkIssuePayload(command, issue));
5101
+ if (command.json)
5102
+ console.log(JSON.stringify({
5103
+ issue,
5104
+ task: result2.task,
5105
+ action: result2.created ? "created" : result2.moved ? "moved" : "existing"
5106
+ }, null, 2));
5107
+ else if (result2.created)
5108
+ console.log(`linked issue #${issue.number} to task ${result2.task.id}`);
5109
+ else if (result2.moved)
5110
+ console.log(`moved linked issue #${issue.number} task ${result2.task.id}`);
5111
+ else
5112
+ console.log(`issue #${issue.number} is linked to task ${result2.task.id}`);
5113
+ return;
5114
+ }
5115
+ const serverUrl = await ensureServerUrl(root, server, "/_journal");
5116
+ if (command.kind === "list") {
5117
+ const data = await getJournalData(serverUrl);
5118
+ const entries = selectEntries(data, command.date, command.limit);
5119
+ if (command.json)
5120
+ console.log(JSON.stringify({ version: data.journal.version, entries }, null, 2));
5121
+ else
5122
+ printEntries(data, command.date, command.limit);
5123
+ return;
5124
+ }
5125
+ if (command.kind === "add") {
5126
+ const body = await textFromBody(command, true);
5127
+ const result2 = await journalRequest(serverUrl, "POST", "journal add", {
5128
+ action: "add-entry",
5129
+ date: command.date,
5130
+ title: command.title,
5131
+ labels: command.labels,
5132
+ body
5133
+ });
5134
+ console.log(`added journal entry ${result2.entry.id} for ${result2.entry.date}`);
5135
+ return;
5136
+ }
5137
+ if (command.kind === "edit") {
5138
+ const body = await textFromBody(command, false);
5139
+ const payload2 = {
5140
+ action: "update-entry",
5141
+ id: command.id,
5142
+ date: command.date,
5143
+ title: command.title,
5144
+ labels: command.labels,
5145
+ body
5146
+ };
5147
+ if (await dryRunPayload("update-entry", payload2))
5148
+ return;
5149
+ await journalRequest(serverUrl, "POST", "journal edit", payload2);
5150
+ console.log(`updated journal entry ${command.id}`);
5151
+ return;
5152
+ }
5153
+ if (command.kind === "tasks") {
5154
+ const data = await getJournalData(serverUrl);
5155
+ const tasks = filterJournalTasks(data.tasks, {
5156
+ status: command.status,
5157
+ labels: command.labels,
5158
+ includeClaimed: true
5159
+ });
5160
+ if (command.json)
5161
+ console.log(JSON.stringify({ version: 1, tasks }, null, 2));
5162
+ else
5163
+ printTasks(tasks);
5164
+ return;
5165
+ }
5166
+ if (command.kind === "task-add") {
5167
+ const body = await textFromBody(command, false);
5168
+ const payload2 = {
5169
+ action: "add-task",
5170
+ title: command.title,
5171
+ status: command.status,
5172
+ priority: command.priority,
5173
+ labels: command.labels,
5174
+ due_date: command.dueDate === "" ? null : command.dueDate,
5175
+ source_date: command.sourceDate === "" ? null : command.sourceDate,
5176
+ before_id: command.before,
5177
+ after_id: command.after,
5178
+ position: command.position,
5179
+ body
5180
+ };
5181
+ if (await dryRunPayload("add-task", payload2))
5182
+ return;
5183
+ const result2 = await journalRequest(serverUrl, "POST", "journal task-add", payload2);
5184
+ console.log(`added task ${result2.task.id} ${result2.task.title}`);
5185
+ return;
5186
+ }
5187
+ if (command.kind === "task-update") {
5188
+ const body = await textFromBody(command, false);
5189
+ const payload2 = {
5190
+ action: "update-task",
5191
+ id: command.id,
5192
+ title: command.title,
5193
+ status: command.status,
5194
+ priority: command.priority,
5195
+ labels: command.clearLabels ? [] : command.labels,
5196
+ due_date: command.dueDate === "" ? null : command.dueDate,
5197
+ source_date: command.sourceDate === "" ? null : command.sourceDate,
5198
+ body
5199
+ };
5200
+ if (await dryRunPayload("update-task", payload2))
5201
+ return;
5202
+ await journalRequest(serverUrl, "POST", "journal task-update", payload2);
5203
+ console.log(`updated task ${command.id}`);
5204
+ return;
5205
+ }
5206
+ if (command.kind === "task-next") {
5207
+ const data = await getJournalData(serverUrl);
5208
+ const tasks = selectNextJournalTasks(data.tasks, {
5209
+ status: command.status || "todo",
5210
+ labels: command.labels
5211
+ }, command.limit);
5212
+ if (command.json)
5213
+ console.log(JSON.stringify({ tasks }, null, 2));
5214
+ else
5215
+ printTasks(tasks);
5216
+ return;
5217
+ }
5218
+ if (command.kind === "task-claim") {
5219
+ const payload2 = {
5220
+ action: "claim-task",
5221
+ id: command.id,
5222
+ by: command.by,
5223
+ lease_minutes: command.leaseMinutes,
5224
+ wip_limit: command.wipLimit
5225
+ };
5226
+ if (await dryRunPayload("claim-task", payload2))
5227
+ return;
5228
+ const result2 = await journalRequest(serverUrl, "POST", "journal task-claim", payload2);
5229
+ console.log(`claimed task ${result2.task.id} until ${result2.task.claim?.lease_expires_at}`);
5230
+ return;
5231
+ }
5232
+ if (command.kind === "task-done") {
5233
+ const payload2 = {
5234
+ action: "complete-task",
5235
+ id: command.id,
5236
+ by: command.by,
5237
+ note: textFromNote(command)
5238
+ };
5239
+ if (await dryRunPayload("complete-task", payload2))
5240
+ return;
5241
+ await journalRequest(serverUrl, "POST", "journal task-done", payload2);
5242
+ console.log(`completed task ${command.id}`);
5243
+ return;
5244
+ }
5245
+ const payload = { action: "delete-task", id: command.id };
5246
+ if (await dryRunPayload("delete-task", payload))
5247
+ return;
5248
+ const result = await journalRequest(serverUrl, "POST", "journal task-delete", payload);
5249
+ if (!result.removed) {
5250
+ console.error(`task not found: ${command.id}`);
5251
+ process.exit(1);
5252
+ }
5253
+ console.log(`deleted task ${command.id}`);
5254
+ }
5255
+ var JOURNAL_HELP = `code-viewer journal — daily work journal and task queue
5256
+
5257
+ The journal is stored in <repo>/.code-viewer/daily-journal.json and tasks are
5258
+ stored in <repo>/.code-viewer/tasks.json. A running code-viewer server for the
5259
+ repository is required unless you pass --dry-run for a write command.
5260
+ The github-issues command is read-only and runs gh directly without a server.
4049
5261
 
4050
- code-viewer file blame --path src/sample.ts --json
4051
- code-viewer file blame --path src/sample.ts --base HEAD --json
4052
- code-viewer file history --path src/sample.ts --limit 10 --json
4053
- code-viewer file history --path src/sample.ts --query "author:tester" --json
4054
- code-viewer file show --path src/sample.ts --json
4055
- code-viewer file show --path src/sample.ts --start 100 --end 150 --json
4056
- code-viewer file show --path src/sample.ts --ref main --json
4057
- code-viewer file diff --path src/sample.ts --json
4058
- code-viewer file diff --path src/sample.ts --from main --to HEAD --json
4059
- code-viewer file diff --path src/sample.ts --full --json
4060
- code-viewer file diff --path new_sample.ts --untracked --json
4061
- code-viewer file diff --path renamed.ts --old-path original.ts --from HEAD~1 --to HEAD --json
5262
+ Run "code-viewer journal agent-help" for an AI-agent oriented guide.
4062
5263
 
4063
- ## Output contract
5264
+ Usage:
5265
+ code-viewer journal list [--date <YYYY-MM-DD|today>] [--limit <n>] [--json]
5266
+ code-viewer journal add --date <YYYY-MM-DD|today> [--title <text>]
5267
+ [--label <label>...] [--body <markdown> | --body-file <path>]
5268
+ code-viewer journal edit <id> [--date <YYYY-MM-DD|today>] [--title <text>]
5269
+ [--label <label>...] [--body <markdown> | --body-file <path>]
5270
+ code-viewer journal tasks [--status <draft|todo|doing|blocked|done>]
5271
+ [--label <label>...] [--json]
5272
+ code-viewer journal task-add --title <text>
5273
+ [--status <status>] [--priority <p0|p1|p2|p3>] [--label <label>...]
5274
+ [--due <YYYY-MM-DD|none>] [--source-date <YYYY-MM-DD|today|none>]
5275
+ [--before <id> | --after <id> | --position <n>]
5276
+ [--body <markdown> | --body-file <path>]
5277
+ code-viewer journal task-update <id> [--title <text>] [--status <status>]
5278
+ [--priority <p0|p1|p2|p3>] [--label <label>...] [--clear-labels]
5279
+ [--due <YYYY-MM-DD|none>] [--source-date <YYYY-MM-DD|today|none>]
5280
+ [--body <markdown> | --body-file <path>]
5281
+ code-viewer journal task-next [--label <label>...] [--status <status>]
5282
+ [--limit <n>] [--json]
5283
+ code-viewer journal github-issues [--repo <owner/repo>]
5284
+ [--state <open|closed|all>] [--gh-label <label>...]
5285
+ [--search <query>] [--limit <n>] [--json] [--bin gh=<path>]
5286
+ code-viewer journal task-link-issue <number> [--repo <owner/repo>]
5287
+ [--status <status>] [--priority <p0|p1|p2|p3>] [--label <label>...]
5288
+ [--before <id> | --after <id> | --position <n>] [--json]
5289
+ [--dry-run] [--bin gh=<path>]
5290
+ code-viewer journal task-claim <id> [--by <agent>] [--lease-minutes <n>]
5291
+ [--wip-limit <n>]
5292
+ code-viewer journal task-done <id> --by <agent>
5293
+ [--note <markdown> | --note-file <path>]
5294
+ code-viewer journal task-delete <id>
4064
5295
 
4065
- --json shapes (mirroring git.ts DTOs verbatim where possible):
5296
+ Global options:
5297
+ --cwd <dir> repository root (default: current directory)
5298
+ --server <url> code-viewer server URL (default: auto-discovered)
5299
+ --bin gh=<p> override gh executable path for github-issues
5300
+ --dry-run print the write payload without sending it
5301
+ `, JOURNAL_AGENT_HELP = `code-viewer journal — agent guide
4066
5302
 
4067
- blame:
4068
- {
4069
- "path": string,
4070
- "ref": string, // the literal ref the CLI resolved to
4071
- "base": "worktree"|"HEAD",
4072
- "result": GitBlameResult // { lines, commits, isUntracked?, isSynthetic?, error? }
4073
- }
4074
- history:
4075
- {
4076
- "path": string,
4077
- "ref": string,
4078
- "limit": number,
4079
- "skip": number,
4080
- "query"?: string,
4081
- "result": { commits: GitHistoryCommit[], hasMore: boolean, error?: string }
4082
- }
4083
- show:
4084
- {
4085
- "path": string,
4086
- "ref": string,
4087
- "start"?: number,
4088
- "end"?: number,
4089
- "totalLines": number, // total lines in the file at <ref>
4090
- "complete": boolean, // true when the returned text covers the whole file
4091
- "text": string
4092
- }
4093
- diff:
4094
- {
4095
- "path": string,
4096
- "old_path"?: string,
4097
- "from": string, // "/dev/null" when untracked is true
4098
- "to": string,
4099
- "untracked": boolean,
4100
- "ignore_ws": boolean,
4101
- "ignore_blank": boolean,
4102
- "mode": "preview"|"full",
4103
- "max_hunks": number|null, // null in full mode
4104
- "max_lines": number|null, // null in full mode
4105
- "diff": string, // unified diff text
4106
- "hunk_count": number, // total hunks before truncation
4107
- "rendered_hunk_count": number, // hunks actually included in diff
4108
- "line_count": number, // total newlines in the returned diff
4109
- "truncated": boolean, // true when preview dropped hunks/lines
4110
- "binary": boolean, // git emitted "Binary files ..."
4111
- "error"?: string // git stderr (also exits 1)
4112
- }
5303
+ You are an AI coding agent. Use this tool to write daily work notes and process
5304
+ explicit task queues without guessing from memory.
4113
5305
 
4114
- Default (non --json) output:
5306
+ ## Workflow
4115
5307
 
4116
- blame: one line per source line → <line><TAB><shortSha or "worktree"><TAB><summary>
4117
- uncommitted edits show as "worktree" and "<uncommitted>".
4118
- history: one line per commit → <shortSha><TAB><whenISO><TAB><author><TAB><subject>
4119
- 0 commits prints "no history" to stderr, exit 0.
4120
- show: the file (or sliced lines) as text. 0-byte files succeed (exit 0).
4121
- diff: unified diff text (the same bytes the JSON "diff" field carries).
4122
- Empty (or worktree==worktree) returns nothing on stdout, exit 0.
5308
+ 1. Check the queue before doing label-scoped work:
5309
+ code-viewer journal task-next --label ai-ready --limit 5 --json
5310
+ 2. Claim exactly one task before editing code:
5311
+ code-viewer journal task-claim <task-id> --by agent --wip-limit 1
5312
+ 3. When finished, mark it done with a short note:
5313
+ code-viewer journal task-done <task-id> --by agent --note "Implemented and verified."
5314
+ 4. Add a daily journal entry for work that is not already represented by a task:
5315
+ code-viewer journal add --date today --label ai --body "..."
5316
+ 5. Inspect GitHub issues read-only before deciding what local task to create:
5317
+ code-viewer journal github-issues --repo owner/repo --json
5318
+ 6. Link a GitHub issue to the local board without updating GitHub:
5319
+ code-viewer journal task-link-issue 123 --repo owner/repo --status draft --label ai-ready
4123
5320
 
4124
- ## Tips
5321
+ ## Rules
4125
5322
 
4126
- - Prefer --json. blame DTO carries author / authorMail / authorTime per
4127
- commit; the plain-text format only shows summary.
4128
- - For huge files, slice with --start/--end before piping to a model
4129
- context "totalLines" / "complete" tell you what was dropped.
4130
- - file show reads the worktree by default. Pass --ref HEAD / --ref <branch>
4131
- when you need a committed snapshot.
4132
- - A non-fatal git failure (e.g. unknown ref, unsafe path) returns the
4133
- error string inside the JSON "result.error" / "error" field AND exits 1.
4134
- - file diff defaults to preview mode (${FILE_DIFF_DEFAULT_MAX_HUNKS} hunks /
4135
- ${FILE_DIFF_DEFAULT_MAX_LINES} lines). Use --full only when you need every
4136
- hunk — preview matches the browser's initial paint and is almost always
4137
- enough context for an LLM. In full mode, max_hunks / max_lines are null in
4138
- JSON because no preview cap is applied. Worktree == Worktree range returns
4139
- "" instantly.
4140
- - Use "code-viewer search code" to discover the path first, then drill
4141
- in with blame / history / show / diff.
5323
+ - Treat labels as filters, priority as importance, and card order as human order.
5324
+ - Do not process draft tasks unless the human asked for drafts.
5325
+ - Use task-next for ordering; do not sort the JSON yourself unless asked.
5326
+ - Use --dry-run before large generated entries.
5327
+ - GitHub issue listing is read-only. Create or update local tasks explicitly.
5328
+ - task-link-issue reads issue metadata only and stores a local task link plus
5329
+ your local labels. It does not copy the issue body or update GitHub.
4142
5330
  `;
4143
- VALUE_FLAGS = new Set([
4144
- "--path",
4145
- "--ref",
4146
- "--base",
4147
- "--limit",
4148
- "--skip",
4149
- "--query",
4150
- "--start",
4151
- "--end",
4152
- "--from",
4153
- "--to",
4154
- "--old-path",
4155
- "--max-hunks",
4156
- "--max-lines"
4157
- ]);
4158
- BOOL_FLAGS = new Set([
4159
- "--json",
4160
- "--untracked",
4161
- "--ignore-ws",
4162
- "--ignore-blank",
4163
- "--full"
4164
- ]);
5331
+ var init_journal_cli = __esm(() => {
5332
+ init_journal();
5333
+ init_cli_helpers();
5334
+ init_command_resolver();
5335
+ init_github_issues();
4165
5336
  });
4166
5337
 
4167
5338
  // web-src/core/routes.ts
@@ -4214,6 +5385,19 @@ function buildRoute(route) {
4214
5385
  const qs = params.toString();
4215
5386
  return `/history${qs ? `?${qs}` : ""}`;
4216
5387
  }
5388
+ case "journal": {
5389
+ const params = new URLSearchParams;
5390
+ if (route.tab && route.tab !== "journal")
5391
+ params.set("tab", route.tab);
5392
+ if (route.date)
5393
+ params.set("date", route.date);
5394
+ if (route.label)
5395
+ params.set("label", route.label);
5396
+ if (route.task)
5397
+ params.set("task", route.task);
5398
+ const qs = params.toString();
5399
+ return `/journal${qs ? `?${qs}` : ""}`;
5400
+ }
4217
5401
  case "database": {
4218
5402
  const params = new URLSearchParams;
4219
5403
  if (route.db)
@@ -4245,6 +5429,7 @@ var init_routes = __esm(() => {
4245
5429
  "/file",
4246
5430
  "/help",
4247
5431
  "/history",
5432
+ "/journal",
4248
5433
  "/database",
4249
5434
  "/doctor"
4250
5435
  ];
@@ -7494,9 +8679,10 @@ missing bundled skills).
7494
8679
  ## What gets installed
7495
8680
 
7496
8681
  The bundled set covers code-viewer's own AI workflows (annotation
7497
- walkthroughs, doctor introspection, query/snapshot inspection). After
7498
- install, ask the AI to use \`code-viewer annotate\` and the related
7499
- skills will guide it.
8682
+ walkthroughs, Work Log task queues, doctor introspection, and
8683
+ query/snapshot inspection). After install, ask the AI to use
8684
+ \`code-viewer annotate\`, \`code-viewer journal\`, or the related
8685
+ commands and the skills will guide it.
7500
8686
  `;
7501
8687
  var init_skill_cli = __esm(() => {
7502
8688
  init_root();
@@ -7954,6 +9140,7 @@ var AGENT_GUIDES;
7954
9140
  var init_agent_help = __esm(() => {
7955
9141
  init_annotate_cli();
7956
9142
  init_file_cli();
9143
+ init_journal_cli();
7957
9144
  init_query_cli();
7958
9145
  init_search_cli();
7959
9146
  init_skill_cli();
@@ -7974,6 +9161,11 @@ var init_agent_help = __esm(() => {
7974
9161
  signature: firstLine(ANNOTATE_AGENT_HELP),
7975
9162
  rerun: "code-viewer annotate agent-help"
7976
9163
  },
9164
+ {
9165
+ name: "journal",
9166
+ signature: firstLine(JOURNAL_AGENT_HELP),
9167
+ rerun: "code-viewer journal agent-help"
9168
+ },
7977
9169
  {
7978
9170
  name: "search",
7979
9171
  signature: firstLine(SEARCH_AGENT_HELP),
@@ -10597,9 +11789,22 @@ var init_source_meta = __esm(() => {
10597
11789
  hcl: "terraform",
10598
11790
  xml: "xml",
10599
11791
  html: "xml",
10600
- vue: "xml",
11792
+ vue: "vue",
11793
+ svelte: "svelte",
11794
+ astro: "astro",
11795
+ erb: "erb",
11796
+ rhtml: "erb",
11797
+ ejs: "html",
11798
+ hbs: "handlebars",
11799
+ mustache: "handlebars",
11800
+ liquid: "liquid",
11801
+ pug: "pug",
11802
+ twig: "twig",
11803
+ haml: "haml",
10601
11804
  css: "css",
10602
11805
  scss: "scss",
11806
+ sass: "sass",
11807
+ less: "less",
10603
11808
  md: "markdown",
10604
11809
  dockerfile: "dockerfile",
10605
11810
  proto: "protobuf",
@@ -10642,7 +11847,26 @@ var init_source_meta = __esm(() => {
10642
11847
  mm: "objective-cpp",
10643
11848
  tex: "tex",
10644
11849
  bib: "bibtex",
10645
- rst: "rst"
11850
+ rst: "rst",
11851
+ graphql: "graphql",
11852
+ graphqls: "graphql",
11853
+ gql: "graphql",
11854
+ ps1: "powershell",
11855
+ psm1: "powershell",
11856
+ psd1: "powershell",
11857
+ ini: "ini",
11858
+ conf: "ini",
11859
+ env: "dotenv",
11860
+ prisma: "prisma",
11861
+ pas: "pascal",
11862
+ adoc: "asciidoc",
11863
+ asciidoc: "asciidoc",
11864
+ jsonc: "jsonc",
11865
+ mts: "typescript",
11866
+ cts: "typescript",
11867
+ kts: "kotlin",
11868
+ cxx: "cpp",
11869
+ hxx: "cpp"
10646
11870
  };
10647
11871
  TEXT_SOURCE_EXTENSIONS = new Set([
10648
11872
  ...Object.keys(EXT_TO_LANG),
@@ -10794,6 +12018,12 @@ var init_source_meta = __esm(() => {
10794
12018
  "rakefile",
10795
12019
  "procfile",
10796
12020
  "brewfile",
12021
+ "guardfile",
12022
+ "capfile",
12023
+ "vagrantfile",
12024
+ "podfile",
12025
+ "fastfile",
12026
+ "berksfile",
10797
12027
  "gnumakefile",
10798
12028
  "bsdmakefile",
10799
12029
  ".gitattributes",
@@ -17135,7 +18365,7 @@ var init_handle = __esm(() => {
17135
18365
  });
17136
18366
 
17137
18367
  // web-src/server/doctor.ts
17138
- import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync5, statSync as statSync5 } from "node:fs";
18368
+ import { accessSync as accessSync2, constants as constants2, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
17139
18369
  import { dirname as dirname4, join as join14, relative as relative5 } from "node:path";
17140
18370
  import { fileURLToPath as fileURLToPath2 } from "node:url";
17141
18371
  function statusWorse(a, b) {
@@ -17232,7 +18462,7 @@ function findCodeViewerPackageJson() {
17232
18462
  for (let depth = 0;depth < 8; depth += 1) {
17233
18463
  const candidate = join14(cursor, "package.json");
17234
18464
  try {
17235
- const raw = readFileSync5(candidate, "utf8");
18465
+ const raw = readFileSync6(candidate, "utf8");
17236
18466
  const pkg = JSON.parse(raw);
17237
18467
  if (pkg.name === "@youtyan/code-viewer") {
17238
18468
  return { version: pkg.version, path: candidate };
@@ -17439,6 +18669,36 @@ async function checkGit(cwd, signal) {
17439
18669
  }
17440
18670
  return { id: "git", title: "Git", rows };
17441
18671
  }
18672
+ async function checkGithubCli(signal) {
18673
+ const versionRes = await runCached(versionCache, TTL.version, commandForExternal("gh"), ["--version"], TIMEOUT.version, signal);
18674
+ if (!versionRes || versionRes.code !== 0) {
18675
+ return {
18676
+ id: "github",
18677
+ title: "GitHub CLI",
18678
+ rows: [
18679
+ {
18680
+ id: "github.gh",
18681
+ title: "gh binary",
18682
+ status: "warn",
18683
+ detail: versionRes && isCommandNotFoundResult("gh", versionRes) ? commandNotFoundDetail("gh") : "gh --version failed",
18684
+ hint: "GitHub issue listing and issue-to-task linking require gh. Install GitHub CLI or pass --bin gh=/absolute/path."
18685
+ }
18686
+ ]
18687
+ };
18688
+ }
18689
+ return {
18690
+ id: "github",
18691
+ title: "GitHub CLI",
18692
+ rows: [
18693
+ {
18694
+ id: "github.gh",
18695
+ title: "gh binary",
18696
+ status: "ok",
18697
+ detail: firstLine2(versionRes.stdout)
18698
+ }
18699
+ ]
18700
+ };
18701
+ }
17442
18702
  async function detectComposeBinary(signal) {
17443
18703
  const v2 = await runCached(versionCache, TTL.version, commandForExternal("docker"), ["compose", "version", "--short"], TIMEOUT.version, signal);
17444
18704
  if (v2 && v2.code === 0) {
@@ -18020,6 +19280,7 @@ async function buildDoctorReport(ctx) {
18020
19280
  const sqlite = await checkSqlite(ctx.cwd);
18021
19281
  const snapshot = checkSnapshotStore(ctx.cwd);
18022
19282
  const git = await checkGit(ctx.cwd, ctx.signal);
19283
+ const github = await checkGithubCli(ctx.signal);
18023
19284
  const discovery = await checkDiscovery(ctx.cwd, ctx.scopeOmitDirNames, ctx.signal);
18024
19285
  const docker = await checkDocker(ctx.signal, discovery.dockerResult);
18025
19286
  const datastore = await checkDatastoreConnectivity(ctx.cwd, ctx.scopeOmitDirNames, ctx.signal);
@@ -18030,6 +19291,7 @@ async function buildDoctorReport(ctx) {
18030
19291
  sqlite,
18031
19292
  snapshot,
18032
19293
  git,
19294
+ github,
18033
19295
  discovery.group,
18034
19296
  datastore,
18035
19297
  docker,
@@ -18139,7 +19401,8 @@ function parseDoctorCliArgs(argv) {
18139
19401
  }
18140
19402
  const parsed = parseExternalCommandOverride(next, "--bin", [
18141
19403
  "git",
18142
- "docker"
19404
+ "docker",
19405
+ "gh"
18143
19406
  ]);
18144
19407
  if (parsed.ok === false)
18145
19408
  return { kind: "error", message: parsed.error };
@@ -18225,7 +19488,7 @@ async function runDoctorCli(argv) {
18225
19488
  const commandConfig = configureExternalCommands({
18226
19489
  cwd,
18227
19490
  cliOverrides: commandOverrides,
18228
- allowedNames: ["git", "docker"]
19491
+ allowedNames: ["git", "docker", "gh"]
18229
19492
  });
18230
19493
  if (commandConfig.ok === false) {
18231
19494
  process.stderr.write(`code-viewer doctor: ${commandConfig.error}
@@ -18251,13 +19514,13 @@ async function runDoctorCli(argv) {
18251
19514
  var DOCTOR_HELP = `code-viewer doctor — diagnose the current environment
18252
19515
 
18253
19516
  Usage:
18254
- code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
19517
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker|gh>=<path>]
18255
19518
  code-viewer doctor agent-help
18256
19519
 
18257
19520
  Options:
18258
19521
  --cwd <path> Working directory to inspect (default: process.cwd()).
18259
19522
  --port <N> Listening port to mention in the report (default: 0 = no server).
18260
- --bin <n>=<p> Override git/docker executable path. Repeatable.
19523
+ --bin <n>=<p> Override git/docker/gh executable path. Repeatable.
18261
19524
  --json Print the full DoctorReport as JSON instead of a summary.
18262
19525
  --help, -h Show this help.
18263
19526
 
@@ -18302,65 +19565,702 @@ import { join as join15 } from "node:path";
18302
19565
  function cacheFresh(cached, now = Date.now(), ttlMs = CACHE_TTL_MS) {
18303
19566
  return !!cached && now - cached.storedAt <= ttlMs;
18304
19567
  }
18305
- function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
18306
- cache.set(key, { ...value, storedAt: now });
18307
- while (cache.size > maxEntries) {
18308
- const oldest = cache.keys().next().value;
18309
- if (oldest === undefined)
19568
+ function setTimedCacheEntry(cache, key, value, now = Date.now(), maxEntries = MAX_TIMED_CACHE_ENTRIES) {
19569
+ cache.set(key, { ...value, storedAt: now });
19570
+ while (cache.size > maxEntries) {
19571
+ const oldest = cache.keys().next().value;
19572
+ if (oldest === undefined)
19573
+ break;
19574
+ cache.delete(oldest);
19575
+ }
19576
+ }
19577
+ function worktreeFileSignature(path, cwd) {
19578
+ try {
19579
+ const stats = lstatSync3(join15(cwd, path));
19580
+ const inode = "ino" in stats ? stats.ino : 0;
19581
+ return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
19582
+ } catch {
19583
+ return "state:missing";
19584
+ }
19585
+ }
19586
+ function fileDiffCacheKey(options) {
19587
+ const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
19588
+ if (options.isUntracked && !worktreeTarget) {
19589
+ throw new Error("untracked file diffs require a worktree range");
19590
+ }
19591
+ const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
19592
+ if (options.isUntracked) {
19593
+ return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
19594
+ }
19595
+ return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
19596
+ }
19597
+ var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
19598
+ var init_cache = () => {};
19599
+
19600
+ // web-src/server/dev-assets.ts
19601
+ import { basename as basename2 } from "node:path";
19602
+ function startDevAssetReload(options) {
19603
+ if (!options.enabled)
19604
+ return false;
19605
+ const watched = new Set(options.watchedFiles);
19606
+ const setTimer = options.setTimeoutFn || setTimeout;
19607
+ const clearTimer = options.clearTimeoutFn || clearTimeout;
19608
+ const debounceMs = options.debounceMs ?? 150;
19609
+ let timer = null;
19610
+ options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
19611
+ if (!filename || !watched.has(basename2(filename.toString())))
19612
+ return;
19613
+ if (timer)
19614
+ clearTimer(timer);
19615
+ timer = setTimer(() => {
19616
+ timer = null;
19617
+ options.sendReload();
19618
+ }, debounceMs);
19619
+ });
19620
+ return true;
19621
+ }
19622
+ var init_dev_assets = () => {};
19623
+
19624
+ // web-src/server/journal.ts
19625
+ import { join as join16 } from "node:path";
19626
+ function dailyJournalFilePath(root) {
19627
+ return join16(root, CODE_VIEWER_DIR, DAILY_JOURNAL_FILE_NAME);
19628
+ }
19629
+ function journalTasksFilePath(root) {
19630
+ return join16(root, CODE_VIEWER_DIR, JOURNAL_TASKS_FILE_NAME);
19631
+ }
19632
+ function emptyDailyJournalState() {
19633
+ return { version: 1, entries: [] };
19634
+ }
19635
+ function emptyJournalTaskState() {
19636
+ return { version: 1, tasks: [] };
19637
+ }
19638
+ function makeJournalId(prefix) {
19639
+ const random = Math.random().toString(36).slice(2, 8);
19640
+ const time = Date.now().toString(36);
19641
+ return `${prefix}-${time}${random}`;
19642
+ }
19643
+ function optionalString3(value, maxLen) {
19644
+ if (typeof value !== "string")
19645
+ return;
19646
+ if (value.includes("\x00"))
19647
+ return;
19648
+ const trimmed = value.trim();
19649
+ if (!trimmed)
19650
+ return;
19651
+ return trimmed.slice(0, maxLen);
19652
+ }
19653
+ function optionalBody(value, maxBytes) {
19654
+ if (typeof value !== "string")
19655
+ return;
19656
+ if (value.includes("\x00"))
19657
+ return;
19658
+ if (Buffer.byteLength(value, "utf8") > maxBytes)
19659
+ return;
19660
+ return value;
19661
+ }
19662
+ function normalizeSource(value) {
19663
+ return value === "ai" || value === "imported" ? value : "user";
19664
+ }
19665
+ function normalizeJournalEntry(raw) {
19666
+ if (!raw || typeof raw !== "object")
19667
+ return null;
19668
+ const entry = raw;
19669
+ const id = optionalString3(entry.id, 128);
19670
+ const date = isIsoDate(entry.date) ? entry.date : undefined;
19671
+ const body = optionalBody(entry.body, JOURNAL_ENTRY_BODY_MAX_BYTES);
19672
+ if (!id || !date || body === undefined)
19673
+ return null;
19674
+ const title = optionalString3(entry.title, JOURNAL_TITLE_MAX_CHARS);
19675
+ return {
19676
+ id,
19677
+ date,
19678
+ ...title ? { title } : {},
19679
+ body,
19680
+ labels: normalizeJournalLabels(entry.labels),
19681
+ source: normalizeSource(entry.source),
19682
+ created_at: optionalString3(entry.created_at, 64) ?? new Date(0).toISOString(),
19683
+ updated_at: optionalString3(entry.updated_at, 64) ?? new Date(0).toISOString()
19684
+ };
19685
+ }
19686
+ function normalizeDailyJournalState(raw) {
19687
+ if (!raw || typeof raw !== "object")
19688
+ return emptyDailyJournalState();
19689
+ const entriesRaw = raw.entries;
19690
+ if (!Array.isArray(entriesRaw))
19691
+ return emptyDailyJournalState();
19692
+ const entries = [];
19693
+ for (const rawEntry of entriesRaw) {
19694
+ if (entries.length >= MAX_ENTRIES3)
19695
+ break;
19696
+ const entry = normalizeJournalEntry(rawEntry);
19697
+ if (entry)
19698
+ entries.push(entry);
19699
+ }
19700
+ entries.sort((a, b) => a.date.localeCompare(b.date));
19701
+ return { version: 1, entries };
19702
+ }
19703
+ function normalizeTaskNote(raw) {
19704
+ if (!raw || typeof raw !== "object")
19705
+ return null;
19706
+ const note = raw;
19707
+ const id = optionalString3(note.id, 128);
19708
+ const body = optionalBody(note.body, JOURNAL_TASK_NOTE_MAX_BYTES);
19709
+ if (!id || body === undefined)
19710
+ return null;
19711
+ return {
19712
+ id,
19713
+ at: optionalString3(note.at, 64) ?? new Date(0).toISOString(),
19714
+ body,
19715
+ source: normalizeSource(note.source)
19716
+ };
19717
+ }
19718
+ function normalizeTaskClaim(raw) {
19719
+ if (!raw || typeof raw !== "object")
19720
+ return;
19721
+ const claim = raw;
19722
+ const by = optionalString3(claim.by, 128);
19723
+ const claimedAt = optionalString3(claim.claimed_at, 64);
19724
+ const leaseExpiresAt = optionalString3(claim.lease_expires_at, 64);
19725
+ if (!by || !claimedAt || !leaseExpiresAt)
19726
+ return;
19727
+ return {
19728
+ by,
19729
+ claimed_at: claimedAt,
19730
+ lease_expires_at: leaseExpiresAt
19731
+ };
19732
+ }
19733
+ function normalizeJournalTask(raw) {
19734
+ if (!raw || typeof raw !== "object")
19735
+ return null;
19736
+ const task = raw;
19737
+ const id = optionalString3(task.id, 128);
19738
+ const title = optionalString3(task.title, JOURNAL_TITLE_MAX_CHARS);
19739
+ if (!id || !title)
19740
+ return null;
19741
+ const status = isJournalTaskStatus(task.status) ? task.status : "todo";
19742
+ const priority = isJournalTaskPriority(task.priority) ? task.priority : "p2";
19743
+ const body = optionalBody(task.body, JOURNAL_TASK_BODY_MAX_BYTES) ?? "";
19744
+ const dueDate = isIsoDate(task.due_date) ? task.due_date : undefined;
19745
+ const sourceDate = isIsoDate(task.source_date) ? task.source_date : undefined;
19746
+ const journalEntryId = optionalString3(task.journal_entry_id, 128);
19747
+ const completedAt = optionalString3(task.completed_at, 64);
19748
+ const notes = Array.isArray(task.notes) ? task.notes.slice(0, MAX_NOTES_PER_TASK).map(normalizeTaskNote).filter((note) => note !== null) : [];
19749
+ const claim = normalizeTaskClaim(task.claim);
19750
+ return {
19751
+ id,
19752
+ title,
19753
+ body,
19754
+ status,
19755
+ priority,
19756
+ labels: normalizeJournalLabels(task.labels),
19757
+ created_at: optionalString3(task.created_at, 64) ?? new Date(0).toISOString(),
19758
+ updated_at: optionalString3(task.updated_at, 64) ?? new Date(0).toISOString(),
19759
+ ...dueDate ? { due_date: dueDate } : {},
19760
+ ...sourceDate ? { source_date: sourceDate } : {},
19761
+ ...journalEntryId ? { journal_entry_id: journalEntryId } : {},
19762
+ ...completedAt ? { completed_at: completedAt } : {},
19763
+ ...claim ? { claim } : {},
19764
+ ...notes.length ? { notes } : {}
19765
+ };
19766
+ }
19767
+ function normalizeJournalTaskState(raw) {
19768
+ if (!raw || typeof raw !== "object")
19769
+ return emptyJournalTaskState();
19770
+ const tasksRaw = raw.tasks;
19771
+ if (!Array.isArray(tasksRaw))
19772
+ return emptyJournalTaskState();
19773
+ const tasks = [];
19774
+ for (const rawTask of tasksRaw) {
19775
+ if (tasks.length >= MAX_TASKS)
18310
19776
  break;
18311
- cache.delete(oldest);
19777
+ const task = normalizeJournalTask(rawTask);
19778
+ if (task)
19779
+ tasks.push(task);
18312
19780
  }
19781
+ return { version: 1, tasks };
18313
19782
  }
18314
- function worktreeFileSignature(path, cwd) {
18315
- try {
18316
- const stats = lstatSync3(join15(cwd, path));
18317
- const inode = "ino" in stats ? stats.ino : 0;
18318
- return `state:file|size:${stats.size}|mtime:${stats.mtimeMs}|ctime:${stats.ctimeMs}|ino:${inode}`;
18319
- } catch {
18320
- return "state:missing";
19783
+ async function loadDailyJournalState(root) {
19784
+ return dailyJournalStore.load(root);
19785
+ }
19786
+ async function loadJournalTaskState(root) {
19787
+ return journalTaskStore.load(root);
19788
+ }
19789
+ async function updateDailyJournalState(root, updater) {
19790
+ return dailyJournalStore.update(root, updater);
19791
+ }
19792
+ async function updateJournalTaskState(root, updater) {
19793
+ return journalTaskStore.update(root, updater);
19794
+ }
19795
+ function insertOptionCount2(input) {
19796
+ return (input.before_id ? 1 : 0) + (input.after_id ? 1 : 0) + (input.position !== undefined ? 1 : 0);
19797
+ }
19798
+ function taskInsertIndex(tasks, status, input) {
19799
+ if (insertOptionCount2(input) > 1)
19800
+ return { ok: false, error: "use only one of before, after, or position" };
19801
+ if (input.before_id || input.after_id) {
19802
+ const anchorId = input.before_id || input.after_id || "";
19803
+ const anchorIndex = tasks.findIndex((task) => task.id === anchorId);
19804
+ const anchor = tasks[anchorIndex];
19805
+ if (!anchor)
19806
+ return { ok: false, error: "anchor task not found" };
19807
+ if (anchor.status !== status)
19808
+ return { ok: false, error: "anchor task belongs to another column" };
19809
+ return {
19810
+ ok: true,
19811
+ index: input.before_id ? anchorIndex : anchorIndex + 1
19812
+ };
18321
19813
  }
19814
+ const statusIndexes = tasks.map((task, index) => ({ task, index })).filter((item) => item.task.status === status).map((item) => item.index);
19815
+ if (input.position !== undefined) {
19816
+ if (!Number.isInteger(input.position) || input.position < 1)
19817
+ return { ok: false, error: "position must be a positive integer" };
19818
+ if (input.position > statusIndexes.length + 1)
19819
+ return { ok: false, error: "position is out of range" };
19820
+ if (input.position <= statusIndexes.length)
19821
+ return { ok: true, index: statusIndexes[input.position - 1] };
19822
+ }
19823
+ const last = statusIndexes[statusIndexes.length - 1];
19824
+ return { ok: true, index: last === undefined ? tasks.length : last + 1 };
19825
+ }
19826
+ function validateEntryInput(input) {
19827
+ if (!isIsoDate(input.date))
19828
+ return { ok: false, error: "date must be YYYY-MM-DD" };
19829
+ if (optionalBody(input.body, JOURNAL_ENTRY_BODY_MAX_BYTES) === undefined)
19830
+ return { ok: false, error: "body is required or too large" };
19831
+ if (!input.body.trim())
19832
+ return { ok: false, error: "body is required" };
19833
+ return { ok: true };
18322
19834
  }
18323
- function fileDiffCacheKey(options) {
18324
- const worktreeTarget = options.range.from === "worktree" || !options.range.to || options.range.to === "worktree";
18325
- if (options.isUntracked && !worktreeTarget) {
18326
- throw new Error("untracked file diffs require a worktree range");
19835
+ function addDailyJournalEntry(state, input, now, makeId3 = makeJournalId) {
19836
+ const valid = validateEntryInput(input);
19837
+ if (valid.ok === false)
19838
+ return valid;
19839
+ const title = optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS);
19840
+ const entry = {
19841
+ id: makeId3("j"),
19842
+ date: input.date,
19843
+ ...title ? { title } : {},
19844
+ body: input.body,
19845
+ labels: normalizeJournalLabels(input.labels),
19846
+ source: input.source || "user",
19847
+ created_at: now,
19848
+ updated_at: now
19849
+ };
19850
+ return {
19851
+ ok: true,
19852
+ state: { version: 1, entries: [...state.entries, entry] },
19853
+ entry
19854
+ };
19855
+ }
19856
+ function updateDailyJournalEntry(state, id, patch, now) {
19857
+ const entry = state.entries.find((item) => item.id === id);
19858
+ if (!entry)
19859
+ return { ok: false, error: "journal entry not found" };
19860
+ const next = {
19861
+ ...entry,
19862
+ updated_at: now
19863
+ };
19864
+ if (patch.date !== undefined) {
19865
+ if (!isIsoDate(patch.date))
19866
+ return { ok: false, error: "date must be YYYY-MM-DD" };
19867
+ next.date = patch.date;
19868
+ }
19869
+ if (patch.title !== undefined) {
19870
+ const title = optionalString3(patch.title, JOURNAL_TITLE_MAX_CHARS);
19871
+ if (title)
19872
+ next.title = title;
19873
+ else
19874
+ delete next.title;
18327
19875
  }
18328
- const signature = worktreeTarget ? `\x00${worktreeFileSignature(options.path, options.cwd)}` : "";
18329
- if (options.isUntracked) {
18330
- return `u\x00${options.path}${signature}\x00${options.extras.join("\x00")}`;
19876
+ if (patch.body !== undefined) {
19877
+ const body = optionalBody(patch.body, JOURNAL_ENTRY_BODY_MAX_BYTES);
19878
+ if (body === undefined || !body.trim())
19879
+ return { ok: false, error: "body is required or too large" };
19880
+ next.body = body;
19881
+ }
19882
+ if (patch.labels !== undefined)
19883
+ next.labels = normalizeJournalLabels(patch.labels);
19884
+ if (patch.source !== undefined)
19885
+ next.source = patch.source;
19886
+ return {
19887
+ ok: true,
19888
+ state: {
19889
+ version: 1,
19890
+ entries: state.entries.map((item) => item.id === id ? next : item)
19891
+ },
19892
+ entry: next
19893
+ };
19894
+ }
19895
+ function deleteDailyJournalEntry(state, id) {
19896
+ const entries = state.entries.filter((entry) => entry.id !== id);
19897
+ return {
19898
+ state: { version: 1, entries },
19899
+ removed: entries.length !== state.entries.length
19900
+ };
19901
+ }
19902
+ function validateTaskInput(input) {
19903
+ if (!optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS))
19904
+ return { ok: false, error: "title is required" };
19905
+ if (input.body !== undefined && optionalBody(input.body, JOURNAL_TASK_BODY_MAX_BYTES) === undefined)
19906
+ return { ok: false, error: "body is too large" };
19907
+ if (input.due_date !== undefined && !isIsoDate(input.due_date))
19908
+ return { ok: false, error: "due date must be YYYY-MM-DD" };
19909
+ if (input.source_date !== undefined && !isIsoDate(input.source_date))
19910
+ return { ok: false, error: "source date must be YYYY-MM-DD" };
19911
+ return { ok: true };
19912
+ }
19913
+ function completedAtForStatus(status, previous, now) {
19914
+ return status === "done" ? previous || now : undefined;
19915
+ }
19916
+ function githubIssueTaskBody(issueNumber, issueUrl, memoLabel) {
19917
+ return [
19918
+ `GitHub issue #${issueNumber}`,
19919
+ ...issueUrl ? [issueUrl] : [],
19920
+ "",
19921
+ memoLabel || "Memo:"
19922
+ ].join(`
19923
+ `);
19924
+ }
19925
+ function githubIssueRequiredLabels(issueNumber, repo, labels) {
19926
+ const repoLabel = journalIssueRepoLabel(repo);
19927
+ return normalizeJournalLabels([
19928
+ "github",
19929
+ journalIssueLabel(issueNumber),
19930
+ ...repoLabel ? [repoLabel] : [],
19931
+ ...Array.isArray(labels) ? labels : []
19932
+ ]);
19933
+ }
19934
+ function taskHasPlacement(input, currentStatus) {
19935
+ return !!input.status && input.status !== currentStatus || !!input.before_id || !!input.after_id || input.position !== undefined;
19936
+ }
19937
+ function addJournalTask(state, input, now, makeId3 = makeJournalId) {
19938
+ const valid = validateTaskInput(input);
19939
+ if (valid.ok === false)
19940
+ return valid;
19941
+ const anchorId = input.before_id || input.after_id;
19942
+ const anchor = anchorId ? state.tasks.find((task2) => task2.id === anchorId) : undefined;
19943
+ if (anchorId && !anchor)
19944
+ return { ok: false, error: "anchor task not found" };
19945
+ const status = input.status || anchor?.status || "todo";
19946
+ const task = {
19947
+ id: makeId3("t"),
19948
+ title: optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS) || "Untitled task",
19949
+ body: input.body || "",
19950
+ status,
19951
+ priority: input.priority || "p2",
19952
+ labels: normalizeJournalLabels(input.labels),
19953
+ created_at: now,
19954
+ updated_at: now,
19955
+ ...input.due_date ? { due_date: input.due_date } : {},
19956
+ ...input.source_date ? { source_date: input.source_date } : {},
19957
+ ...input.journal_entry_id ? { journal_entry_id: input.journal_entry_id } : {},
19958
+ ...completedAtForStatus(status, undefined, now) ? { completed_at: now } : {}
19959
+ };
19960
+ const insertAt = taskInsertIndex(state.tasks, status, input);
19961
+ if (insertAt.ok === false)
19962
+ return insertAt;
19963
+ const tasks = [...state.tasks];
19964
+ tasks.splice(insertAt.index, 0, task);
19965
+ return { ok: true, state: { version: 1, tasks }, task };
19966
+ }
19967
+ function updateJournalTask(state, id, patch, now) {
19968
+ const task = state.tasks.find((item) => item.id === id);
19969
+ if (!task)
19970
+ return { ok: false, error: "task not found" };
19971
+ const next = { ...task, updated_at: now };
19972
+ if (patch.title !== undefined) {
19973
+ const title = optionalString3(patch.title, JOURNAL_TITLE_MAX_CHARS);
19974
+ if (!title)
19975
+ return { ok: false, error: "title is required" };
19976
+ next.title = title;
18331
19977
  }
18332
- return `t\x00${options.path}\x00${options.oldPath || ""}${signature}\x00${[...options.extras, ...options.args].join("\x00")}`;
19978
+ if (patch.body !== undefined) {
19979
+ const body = optionalBody(patch.body, JOURNAL_TASK_BODY_MAX_BYTES);
19980
+ if (body === undefined)
19981
+ return { ok: false, error: "body is too large" };
19982
+ next.body = body;
19983
+ }
19984
+ if (patch.status !== undefined) {
19985
+ next.status = patch.status;
19986
+ const completedAt = completedAtForStatus(patch.status, next.completed_at, now);
19987
+ if (completedAt)
19988
+ next.completed_at = completedAt;
19989
+ else
19990
+ delete next.completed_at;
19991
+ if (patch.status !== "doing")
19992
+ delete next.claim;
19993
+ }
19994
+ if (patch.priority !== undefined)
19995
+ next.priority = patch.priority;
19996
+ if (patch.labels !== undefined)
19997
+ next.labels = normalizeJournalLabels(patch.labels);
19998
+ if (patch.due_date !== undefined) {
19999
+ if (patch.due_date === null || patch.due_date === "")
20000
+ delete next.due_date;
20001
+ else if (isIsoDate(patch.due_date))
20002
+ next.due_date = patch.due_date;
20003
+ else
20004
+ return { ok: false, error: "due date must be YYYY-MM-DD" };
20005
+ }
20006
+ if (patch.source_date !== undefined) {
20007
+ if (patch.source_date === null || patch.source_date === "")
20008
+ delete next.source_date;
20009
+ else if (isIsoDate(patch.source_date))
20010
+ next.source_date = patch.source_date;
20011
+ else
20012
+ return { ok: false, error: "source date must be YYYY-MM-DD" };
20013
+ }
20014
+ if (patch.journal_entry_id !== undefined) {
20015
+ const journalEntryId = optionalString3(patch.journal_entry_id, 128);
20016
+ if (journalEntryId)
20017
+ next.journal_entry_id = journalEntryId;
20018
+ else
20019
+ delete next.journal_entry_id;
20020
+ }
20021
+ return {
20022
+ ok: true,
20023
+ state: {
20024
+ version: 1,
20025
+ tasks: state.tasks.map((item) => item.id === id ? next : item)
20026
+ },
20027
+ task: next
20028
+ };
18333
20029
  }
18334
- var CACHE_TTL_MS = 1500, MAX_TIMED_CACHE_ENTRIES = 200;
18335
- var init_cache = () => {};
18336
-
18337
- // web-src/server/dev-assets.ts
18338
- import { basename as basename2 } from "node:path";
18339
- function startDevAssetReload(options) {
18340
- if (!options.enabled)
18341
- return false;
18342
- const watched = new Set(options.watchedFiles);
18343
- const setTimer = options.setTimeoutFn || setTimeout;
18344
- const clearTimer = options.clearTimeoutFn || clearTimeout;
18345
- const debounceMs = options.debounceMs ?? 150;
18346
- let timer = null;
18347
- options.watch(options.webRoot, { persistent: false }, (_event, filename) => {
18348
- if (!filename || !watched.has(basename2(filename.toString())))
18349
- return;
18350
- if (timer)
18351
- clearTimer(timer);
18352
- timer = setTimer(() => {
18353
- timer = null;
18354
- options.sendReload();
18355
- }, debounceMs);
20030
+ function moveJournalTask(state, id, input, now) {
20031
+ const source = state.tasks.find((task) => task.id === id);
20032
+ if (!source)
20033
+ return { ok: false, error: "task not found" };
20034
+ if (input.before_id === id || input.after_id === id)
20035
+ return { ok: false, error: "cannot move task relative to itself" };
20036
+ const tasksWithoutSource = state.tasks.filter((task) => task.id !== id);
20037
+ const anchorId = input.before_id || input.after_id;
20038
+ const anchor = anchorId ? tasksWithoutSource.find((task) => task.id === anchorId) : undefined;
20039
+ if (anchorId && !anchor)
20040
+ return { ok: false, error: "anchor task not found" };
20041
+ const status = input.status || anchor?.status || source.status;
20042
+ const moved = {
20043
+ ...source,
20044
+ status,
20045
+ updated_at: now
20046
+ };
20047
+ const completedAt = completedAtForStatus(status, moved.completed_at, now);
20048
+ if (completedAt)
20049
+ moved.completed_at = completedAt;
20050
+ else
20051
+ delete moved.completed_at;
20052
+ if (status !== "doing")
20053
+ delete moved.claim;
20054
+ const insertAt = taskInsertIndex(tasksWithoutSource, status, input);
20055
+ if (insertAt.ok === false)
20056
+ return insertAt;
20057
+ const tasks = [...tasksWithoutSource];
20058
+ tasks.splice(insertAt.index, 0, moved);
20059
+ return { ok: true, state: { version: 1, tasks }, task: moved };
20060
+ }
20061
+ function linkGithubIssueTask(state, input, now, makeId3 = makeJournalId) {
20062
+ if (!Number.isInteger(input.issue_number) || input.issue_number < 1) {
20063
+ return { ok: false, error: "issue number must be a positive integer" };
20064
+ }
20065
+ const title = optionalString3(input.title, JOURNAL_TITLE_MAX_CHARS) || `GitHub issue #${input.issue_number}`;
20066
+ const repo = optionalString3(input.repo, 120);
20067
+ const issueUrl = optionalString3(input.url, 240);
20068
+ const memoLabel = optionalString3(input.memo_label, 80);
20069
+ const requiredLabels = githubIssueRequiredLabels(input.issue_number, repo, input.labels);
20070
+ const linkLabel = journalIssueLabel(input.issue_number);
20071
+ const repoLabel = journalIssueRepoLabel(repo);
20072
+ const linked = state.tasks.find((task2) => {
20073
+ if (!task2.labels.includes("github") || !task2.labels.includes(linkLabel)) {
20074
+ return false;
20075
+ }
20076
+ if (repoLabel)
20077
+ return task2.labels.includes(repoLabel);
20078
+ return !task2.labels.some((label) => label.startsWith("repo-"));
18356
20079
  });
18357
- return true;
20080
+ if (!linked) {
20081
+ const hasAnchor = !!input.before_id || !!input.after_id;
20082
+ const result = addJournalTask(state, {
20083
+ title,
20084
+ body: githubIssueTaskBody(input.issue_number, issueUrl, memoLabel),
20085
+ status: input.status || (hasAnchor ? undefined : "draft"),
20086
+ priority: input.priority || "p2",
20087
+ labels: requiredLabels,
20088
+ before_id: input.before_id,
20089
+ after_id: input.after_id,
20090
+ position: input.position
20091
+ }, now, makeId3);
20092
+ if (result.ok === false)
20093
+ return result;
20094
+ return {
20095
+ ok: true,
20096
+ state: result.state,
20097
+ task: result.task,
20098
+ created: true,
20099
+ moved: false
20100
+ };
20101
+ }
20102
+ let nextState = state;
20103
+ let task = linked;
20104
+ let moved = false;
20105
+ const placement = {
20106
+ status: input.status,
20107
+ before_id: input.before_id,
20108
+ after_id: input.after_id,
20109
+ position: input.position
20110
+ };
20111
+ if (taskHasPlacement(placement, task.status)) {
20112
+ const result = moveJournalTask(nextState, task.id, placement, now);
20113
+ if (result.ok === false)
20114
+ return result;
20115
+ nextState = result.state;
20116
+ task = result.task;
20117
+ moved = true;
20118
+ }
20119
+ const labels = normalizeJournalLabels([...task.labels, ...requiredLabels]);
20120
+ const shouldUpdateLabels = labels.length !== task.labels.length || labels.some((label, index) => label !== task.labels[index]);
20121
+ if (input.priority || shouldUpdateLabels) {
20122
+ const result = updateJournalTask(nextState, task.id, {
20123
+ ...input.priority ? { priority: input.priority } : {},
20124
+ ...shouldUpdateLabels ? { labels } : {}
20125
+ }, now);
20126
+ if (result.ok === false)
20127
+ return result;
20128
+ nextState = result.state;
20129
+ task = result.task;
20130
+ }
20131
+ return { ok: true, state: nextState, task, created: false, moved };
20132
+ }
20133
+ function claimJournalTask(state, id, input, now) {
20134
+ const task = state.tasks.find((item) => item.id === id);
20135
+ if (!task)
20136
+ return { ok: false, error: "task not found" };
20137
+ const nowMs = Date.parse(now);
20138
+ const activeClaim = task.claim && Number.isFinite(Date.parse(task.claim.lease_expires_at)) && Date.parse(task.claim.lease_expires_at) > nowMs;
20139
+ if (activeClaim)
20140
+ return { ok: false, error: "task is already claimed" };
20141
+ if (task.status !== "todo" && task.status !== "doing")
20142
+ return {
20143
+ ok: false,
20144
+ error: "only todo or expired doing tasks can be claimed"
20145
+ };
20146
+ const by = optionalString3(input.by, 128) || "ai";
20147
+ const wipLimit = input.wip_limit;
20148
+ if (wipLimit !== undefined && wipLimit > 0) {
20149
+ const activeDoing = state.tasks.filter((item) => {
20150
+ if (item.status !== "doing" || !item.claim)
20151
+ return false;
20152
+ if (item.claim.by !== by)
20153
+ return false;
20154
+ const expires = Date.parse(item.claim.lease_expires_at);
20155
+ return Number.isFinite(expires) && expires > nowMs;
20156
+ }).length;
20157
+ if (activeDoing >= wipLimit)
20158
+ return { ok: false, error: "WIP limit reached" };
20159
+ }
20160
+ const leaseMinutes = Number.isFinite(input.lease_minutes) && input.lease_minutes ? Math.min(1440, Math.max(1, Math.round(input.lease_minutes))) : 120;
20161
+ const leaseExpiresAt = new Date(nowMs + leaseMinutes * 60000).toISOString();
20162
+ const next = {
20163
+ ...task,
20164
+ status: "doing",
20165
+ updated_at: now,
20166
+ claim: {
20167
+ by,
20168
+ claimed_at: now,
20169
+ lease_expires_at: leaseExpiresAt
20170
+ }
20171
+ };
20172
+ return {
20173
+ ok: true,
20174
+ state: {
20175
+ version: 1,
20176
+ tasks: state.tasks.map((item) => item.id === id ? next : item)
20177
+ },
20178
+ task: next
20179
+ };
18358
20180
  }
18359
- var init_dev_assets = () => {};
20181
+ function completeJournalTask(state, id, input, now, makeId3 = makeJournalId) {
20182
+ const task = state.tasks.find((item) => item.id === id);
20183
+ if (!task)
20184
+ return { ok: false, error: "task not found" };
20185
+ if (task.status !== "doing")
20186
+ return { ok: false, error: "only doing tasks can be completed" };
20187
+ const nowMs = Date.parse(now);
20188
+ const activeClaim = task.claim && Number.isFinite(Date.parse(task.claim.lease_expires_at)) && Date.parse(task.claim.lease_expires_at) > nowMs;
20189
+ if (!activeClaim)
20190
+ return { ok: false, error: "task must be claimed before completion" };
20191
+ const by = optionalString3(input.by, 128);
20192
+ if (!by)
20193
+ return { ok: false, error: "task completion requires claim owner" };
20194
+ if (task.claim?.by !== by)
20195
+ return { ok: false, error: "task claim belongs to another agent" };
20196
+ const notes = [...task.notes || []];
20197
+ if (input.note?.trim()) {
20198
+ const body = optionalBody(input.note, JOURNAL_TASK_NOTE_MAX_BYTES);
20199
+ if (body === undefined)
20200
+ return { ok: false, error: "note is too large" };
20201
+ notes.push({
20202
+ id: makeId3("n"),
20203
+ at: now,
20204
+ body,
20205
+ source: input.source || "ai"
20206
+ });
20207
+ if (notes.length > MAX_NOTES_PER_TASK) {
20208
+ notes.splice(0, notes.length - MAX_NOTES_PER_TASK);
20209
+ }
20210
+ }
20211
+ const next = {
20212
+ ...task,
20213
+ status: "done",
20214
+ updated_at: now,
20215
+ completed_at: now,
20216
+ ...notes.length ? { notes } : {}
20217
+ };
20218
+ delete next.claim;
20219
+ return {
20220
+ ok: true,
20221
+ state: {
20222
+ version: 1,
20223
+ tasks: state.tasks.map((item) => item.id === id ? next : item)
20224
+ },
20225
+ task: next
20226
+ };
20227
+ }
20228
+ function deleteJournalTask(state, id) {
20229
+ const tasks = state.tasks.filter((task) => task.id !== id);
20230
+ return {
20231
+ state: { version: 1, tasks },
20232
+ removed: tasks.length !== state.tasks.length
20233
+ };
20234
+ }
20235
+ var DAILY_JOURNAL_FILE_NAME = "daily-journal.json", JOURNAL_TASKS_FILE_NAME = "tasks.json", JOURNAL_ENTRY_BODY_MAX_BYTES, JOURNAL_TASK_BODY_MAX_BYTES, JOURNAL_TASK_NOTE_MAX_BYTES, JOURNAL_TITLE_MAX_CHARS = 200, MAX_DAILY_JOURNAL_JSON_BYTES = 8000000, MAX_JOURNAL_TASKS_JSON_BYTES = 8000000, MAX_ENTRIES3 = 2000, MAX_TASKS = 2000, MAX_NOTES_PER_TASK = 100, dailyJournalStore, journalTaskStore;
20236
+ var init_journal2 = __esm(() => {
20237
+ init_journal();
20238
+ init_annotations();
20239
+ init_json_store();
20240
+ JOURNAL_ENTRY_BODY_MAX_BYTES = 128 * 1024;
20241
+ JOURNAL_TASK_BODY_MAX_BYTES = 128 * 1024;
20242
+ JOURNAL_TASK_NOTE_MAX_BYTES = 64 * 1024;
20243
+ dailyJournalStore = createJsonFileStore({
20244
+ filePath: dailyJournalFilePath,
20245
+ empty: emptyDailyJournalState,
20246
+ sanitize: normalizeDailyJournalState,
20247
+ maxBytes: MAX_DAILY_JOURNAL_JSON_BYTES,
20248
+ backupSuffix: "corrupt",
20249
+ sizeErrorMessage: "daily journal state too large"
20250
+ });
20251
+ journalTaskStore = createJsonFileStore({
20252
+ filePath: journalTasksFilePath,
20253
+ empty: emptyJournalTaskState,
20254
+ sanitize: normalizeJournalTaskState,
20255
+ maxBytes: MAX_JOURNAL_TASKS_JSON_BYTES,
20256
+ backupSuffix: "corrupt",
20257
+ sizeErrorMessage: "journal task state too large"
20258
+ });
20259
+ });
18360
20260
 
18361
20261
  // web-src/server/search-service.ts
18362
- import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync6, realpathSync as realpathSync5 } from "node:fs";
18363
- import { join as join16, relative as relative6 } from "node:path";
20262
+ import { existsSync as existsSync7, lstatSync as lstatSync4, readFileSync as readFileSync7, realpathSync as realpathSync5 } from "node:fs";
20263
+ import { join as join17, relative as relative6 } from "node:path";
18364
20264
  function rgAvailable(cwd) {
18365
20265
  if (rgAvailableCache !== null)
18366
20266
  return rgAvailableCache;
@@ -18381,7 +20281,7 @@ function safeWorktreePath(env, path) {
18381
20281
  return null;
18382
20282
  if (isGitInternalPath(path))
18383
20283
  return null;
18384
- const full = join16(env.cwd, path);
20284
+ const full = join17(env.cwd, path);
18385
20285
  if (!existsSync7(full))
18386
20286
  return null;
18387
20287
  let realCwd;
@@ -18427,7 +20327,7 @@ function grepWorktreeFallback(env, query, max, paths) {
18427
20327
  continue;
18428
20328
  let data;
18429
20329
  try {
18430
- data = readFileSync6(full);
20330
+ data = readFileSync7(full);
18431
20331
  } catch {
18432
20332
  continue;
18433
20333
  }
@@ -18560,8 +20460,8 @@ var init_search_service = __esm(() => {
18560
20460
  });
18561
20461
 
18562
20462
  // web-src/server/mcp.ts
18563
- import { readFileSync as readFileSync7 } from "node:fs";
18564
- import { join as join17 } from "node:path";
20463
+ import { readFileSync as readFileSync8 } from "node:fs";
20464
+ import { join as join18 } from "node:path";
18565
20465
  function defaultMcpTools(options = {}) {
18566
20466
  return [
18567
20467
  {
@@ -20035,7 +21935,7 @@ var init_mcp = __esm(() => {
20035
21935
  init_search_cli();
20036
21936
  init_search_service();
20037
21937
  init_status_cli();
20038
- PACKAGE_VERSION = JSON.parse(readFileSync7(join17(ROOT, "package.json"), "utf8")).version;
21938
+ PACKAGE_VERSION = JSON.parse(readFileSync8(join18(ROOT, "package.json"), "utf8")).version;
20039
21939
  MCP_SERVER_INFO = {
20040
21940
  name: "code-viewer",
20041
21941
  title: "code-viewer",
@@ -20122,7 +22022,7 @@ import {
20122
22022
  lstatSync as lstatSync5,
20123
22023
  mkdirSync as mkdirSync4,
20124
22024
  openSync as openSync2,
20125
- readFileSync as readFileSync8,
22025
+ readFileSync as readFileSync9,
20126
22026
  realpathSync as realpathSync6,
20127
22027
  renameSync,
20128
22028
  statSync as statSync6,
@@ -20131,7 +22031,7 @@ import {
20131
22031
  writeFileSync as writeFileSync2
20132
22032
  } from "node:fs";
20133
22033
  import { homedir as homedir3 } from "node:os";
20134
- import { basename as basename3, dirname as dirname5, extname as extname2, join as join18, relative as relative7 } from "node:path";
22034
+ import { basename as basename3, dirname as dirname5, extname as extname2, join as join19, relative as relative7 } from "node:path";
20135
22035
  function parseCli() {
20136
22036
  const rest = [];
20137
22037
  for (let i = 2;i < process.argv.length; i++) {
@@ -20143,17 +22043,18 @@ Usage:
20143
22043
  code-viewer [--cwd <repo>] [--port <port>] [--open] [--bin <name>=<path>] [git-diff-args...]
20144
22044
  code-viewer status [--cwd <repo>] [--bin git=<path>] [--ref <ref>] [--limit <N>] [--json]
20145
22045
  code-viewer annotate <start|add|add-db|rename|edit|move|list|delete|clear> [options]
22046
+ code-viewer journal <list|add|edit|tasks|task-add|task-update|task-next|github-issues|task-link-issue|task-claim|task-done|task-delete> [options]
20146
22047
  code-viewer query <sources|schemas|schema|columns|ddl|exec|list|clear|snapshot|diff|search|redis|elasticsearch|s3> [options] [--bin git=<path>]
20147
22048
  code-viewer search code --term <text> [--ref <ref>] [--path <p>...] [--regex] [--max <n>] [--json] [--bin git=<path>]
20148
22049
  code-viewer search files --term <pattern> [--ref <ref>] [--max <n>] [--json] [--bin git=<path>]
20149
22050
  code-viewer file <blame|history|show|diff> --path <p> [--ref <ref>] [...subcommand options] [--json] [--bin git=<path>]
20150
22051
  code-viewer skill install [--agent <list>] [--global]
20151
- code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker>=<path>]
22052
+ code-viewer doctor [--cwd <path>] [--port <N>] [--json] [--bin <git|docker|gh>=<path>]
20152
22053
  code-viewer agent-help
20153
22054
  code-viewer help
20154
22055
 
20155
22056
  AI-agent index (start here): code-viewer agent-help
20156
- Subcommand guides (AI agents): code-viewer <status|annotate|query|search|file|skill|doctor> agent-help
22057
+ Subcommand guides (AI agents): code-viewer <status|annotate|journal|query|search|file|skill|doctor> agent-help
20157
22058
 
20158
22059
  Examples:
20159
22060
  code-viewer --open
@@ -20251,7 +22152,7 @@ Examples:
20251
22152
  }
20252
22153
  function warnIfLegacyConfigPresent() {
20253
22154
  try {
20254
- if (existsSync8(join18(cwd, ".code-viewer.json"))) {
22155
+ if (existsSync8(join19(cwd, ".code-viewer.json"))) {
20255
22156
  console.warn("[code-viewer] .code-viewer.json is no longer used; configure scope and upload from Viewer Settings instead. The file can be safely removed.");
20256
22157
  }
20257
22158
  } catch {}
@@ -20358,10 +22259,10 @@ function staticFile(pathname) {
20358
22259
  const spec = map[pathname];
20359
22260
  if (!spec)
20360
22261
  return null;
20361
- const full = join18(WEB_ROOT, spec[0]);
22262
+ const full = join19(WEB_ROOT, spec[0]);
20362
22263
  if (!existsSync8(full))
20363
22264
  return text("not found", 404);
20364
- return new Response(readFileSync8(full), {
22265
+ return new Response(readFileSync9(full), {
20365
22266
  headers: { "Content-Type": spec[1], "Cache-Control": "no-store" }
20366
22267
  });
20367
22268
  }
@@ -20613,7 +22514,7 @@ function safeWorktreePath2(path) {
20613
22514
  return safeWorktreePath(currentSearchEnv(), path);
20614
22515
  }
20615
22516
  function worktreePath(path) {
20616
- return join18(cwd, path);
22517
+ return join19(cwd, path);
20617
22518
  }
20618
22519
  function safeOpenWorktreePath(path) {
20619
22520
  if (path === "") {
@@ -20698,7 +22599,7 @@ function readReadme(target, dirPath) {
20698
22599
  if (!full)
20699
22600
  continue;
20700
22601
  try {
20701
- return { path, text: readFileSync8(full, "utf8") };
22602
+ return { path, text: readFileSync9(full, "utf8") };
20702
22603
  } catch {
20703
22604
  continue;
20704
22605
  }
@@ -20884,7 +22785,7 @@ function handleLog(url) {
20884
22785
  }
20885
22786
  function blamePathKey(p) {
20886
22787
  try {
20887
- const st = statSync6(join18(cwd, p));
22788
+ const st = statSync6(join19(cwd, p));
20888
22789
  return `${st.mtimeMs}:${st.size}`;
20889
22790
  } catch {
20890
22791
  return "missing";
@@ -21378,7 +23279,7 @@ async function handleUploadFiles(req) {
21378
23279
  total += file.size;
21379
23280
  if (total > MAX_UPLOAD_TOTAL_BYTES)
21380
23281
  return text("upload too large", 413);
21381
- const target = join18(realDir, safeName);
23282
+ const target = join19(realDir, safeName);
21382
23283
  if (relative7(realDir, dirname5(target)) !== "")
21383
23284
  return text("invalid filename", 400);
21384
23285
  if (existsSync8(target))
@@ -21500,9 +23401,9 @@ function triggerUpdate(changedPaths) {
21500
23401
  sendSse("update", data);
21501
23402
  }
21502
23403
  function moveMacPathIntoTrash(path) {
21503
- const trashDir = join18(homedir3(), ".Trash");
23404
+ const trashDir = join19(homedir3(), ".Trash");
21504
23405
  const base = basename3(path) || "code-viewer-trash-item";
21505
- const target = join18(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
23406
+ const target = join19(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
21506
23407
  try {
21507
23408
  mkdirSync4(trashDir, { recursive: true });
21508
23409
  renameSync(path, target);
@@ -21544,7 +23445,7 @@ function restoreTrashPath(originalPath, trashPath) {
21544
23445
  if (!existsSync8(trashPath))
21545
23446
  return { ok: false, error: "trash item not found" };
21546
23447
  try {
21547
- const trashRoot = join18(homedir3(), ".Trash");
23448
+ const trashRoot = join19(homedir3(), ".Trash");
21548
23449
  const trashRelative = relative7(trashRoot, trashPath);
21549
23450
  if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
21550
23451
  return { ok: false, error: "invalid trash handle" };
@@ -21694,7 +23595,7 @@ async function handleCreateDirectory(req) {
21694
23595
  const targetPath = dir ? `${dir}/${name}` : name;
21695
23596
  if (!safeRepoPath(targetPath) || isGitInternalPath(targetPath))
21696
23597
  return text("invalid target", 400);
21697
- const target = join18(parent, name);
23598
+ const target = join19(parent, name);
21698
23599
  if (existsSync8(target))
21699
23600
  return text("already exists", 409);
21700
23601
  try {
@@ -21779,6 +23680,282 @@ async function handleMcp(req) {
21779
23680
  }
21780
23681
  return json2(dispatched.body);
21781
23682
  }
23683
+ function journalSse(kind, id) {
23684
+ sendSse("journal", JSON.stringify({ kind, id }));
23685
+ }
23686
+ function bodyString(body, key) {
23687
+ const value = body[key];
23688
+ return typeof value === "string" ? value : undefined;
23689
+ }
23690
+ function bodyStringList(body, key) {
23691
+ const value = body[key];
23692
+ if (!Array.isArray(value))
23693
+ return [];
23694
+ return value.filter((item) => typeof item === "string");
23695
+ }
23696
+ function bodyTaskStatus(body, key) {
23697
+ const value = body[key];
23698
+ if (value === undefined)
23699
+ return;
23700
+ if (isJournalTaskStatus(value))
23701
+ return value;
23702
+ throw new JournalRequestError(`${key} must be draft, todo, doing, blocked, or done`);
23703
+ }
23704
+ function bodyTaskPriority(body, key) {
23705
+ const value = body[key];
23706
+ if (value === undefined)
23707
+ return;
23708
+ if (isJournalTaskPriority(value))
23709
+ return value;
23710
+ throw new JournalRequestError(`${key} must be p0, p1, p2, or p3`);
23711
+ }
23712
+ function bodyNumber(body, key) {
23713
+ const value = body[key];
23714
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
23715
+ }
23716
+ async function handleJournal(req) {
23717
+ if (req.method === "GET") {
23718
+ const journal = await loadDailyJournalState(cwd);
23719
+ const tasks = await loadJournalTaskState(cwd);
23720
+ return json2({
23721
+ generation,
23722
+ journal,
23723
+ tasks,
23724
+ labels: collectJournalLabels(journal, tasks)
23725
+ });
23726
+ }
23727
+ if (req.method !== "POST")
23728
+ return text("method not allowed", 405);
23729
+ if (!sideEffectRequestAllowed(req))
23730
+ return text("forbidden", 403);
23731
+ const contentType = req.headers.get("content-type") || "";
23732
+ if (!/^application\/json(?:;|$)/i.test(contentType))
23733
+ return text("unsupported media type", 415);
23734
+ const maxBytes = Math.max(JOURNAL_ENTRY_BODY_MAX_BYTES, JOURNAL_TASK_BODY_MAX_BYTES) + 8192;
23735
+ const length = Number(req.headers.get("content-length") || "0");
23736
+ if (length > maxBytes)
23737
+ return text("payload too large", 413);
23738
+ let body = {};
23739
+ try {
23740
+ const raw = await req.text();
23741
+ if (raw.length > maxBytes)
23742
+ return text("payload too large", 413);
23743
+ body = JSON.parse(raw);
23744
+ } catch {
23745
+ return text("invalid json", 400);
23746
+ }
23747
+ try {
23748
+ const action = body.action;
23749
+ const now = new Date().toISOString();
23750
+ if (action === "add-entry") {
23751
+ const entry = await updateDailyJournalState(cwd, (state) => {
23752
+ const result = addDailyJournalEntry(state, {
23753
+ date: bodyString(body, "date") || "",
23754
+ title: bodyString(body, "title"),
23755
+ body: bodyString(body, "body") || "",
23756
+ labels: body.labels,
23757
+ source: body.source === "ai" ? "ai" : "user"
23758
+ }, now);
23759
+ if (result.ok === false)
23760
+ throw new JournalRequestError(result.error);
23761
+ return { state: result.state, result: result.entry };
23762
+ });
23763
+ journalSse("add-entry", entry.id);
23764
+ return json2({ ok: true, entry, generation });
23765
+ }
23766
+ if (action === "list-github-issues") {
23767
+ const label = bodyString(body, "label");
23768
+ const labels = bodyStringList(body, "labels");
23769
+ if (label)
23770
+ labels.push(label);
23771
+ const issues = readGithubIssueList({
23772
+ cwd,
23773
+ repo: bodyString(body, "repo"),
23774
+ labels,
23775
+ search: bodyString(body, "search"),
23776
+ state: normalizeGithubIssueListState(bodyString(body, "state")),
23777
+ limit: normalizeGithubIssueListLimit(bodyNumber(body, "limit"))
23778
+ });
23779
+ return json2({ ok: true, issues, generation });
23780
+ }
23781
+ if (action === "update-entry") {
23782
+ const id = bodyString(body, "id") || "";
23783
+ if (!id)
23784
+ return text("invalid id", 400);
23785
+ const entry = await updateDailyJournalState(cwd, (state) => {
23786
+ const result = updateDailyJournalEntry(state, id, {
23787
+ date: bodyString(body, "date"),
23788
+ title: bodyString(body, "title"),
23789
+ body: bodyString(body, "body"),
23790
+ labels: body.labels,
23791
+ source: body.source === "ai" ? "ai" : undefined
23792
+ }, now);
23793
+ if (result.ok === false)
23794
+ throw new JournalRequestError(result.error);
23795
+ return { state: result.state, result: result.entry };
23796
+ });
23797
+ journalSse("update-entry", entry.id);
23798
+ return json2({ ok: true, entry, generation });
23799
+ }
23800
+ if (action === "delete-entry") {
23801
+ const id = bodyString(body, "id") || "";
23802
+ if (!id)
23803
+ return text("invalid id", 400);
23804
+ const removed = await updateDailyJournalState(cwd, (state) => {
23805
+ const result = deleteDailyJournalEntry(state, id);
23806
+ return { state: result.state, result: result.removed };
23807
+ });
23808
+ if (removed)
23809
+ journalSse("delete-entry", id);
23810
+ return json2({ ok: true, removed, generation });
23811
+ }
23812
+ if (action === "link-github-issue") {
23813
+ const result = await updateJournalTaskState(cwd, (state) => {
23814
+ const linked = linkGithubIssueTask(state, {
23815
+ issue_number: bodyNumber(body, "issue_number") || 0,
23816
+ repo: bodyString(body, "repo"),
23817
+ title: bodyString(body, "title"),
23818
+ url: bodyString(body, "url"),
23819
+ memo_label: bodyString(body, "memo_label"),
23820
+ status: bodyTaskStatus(body, "status"),
23821
+ priority: bodyTaskPriority(body, "priority"),
23822
+ labels: body.labels,
23823
+ before_id: bodyString(body, "before_id"),
23824
+ after_id: bodyString(body, "after_id"),
23825
+ position: bodyNumber(body, "position")
23826
+ }, now);
23827
+ if (linked.ok === false)
23828
+ throw new JournalRequestError(linked.error);
23829
+ return { state: linked.state, result: linked };
23830
+ });
23831
+ journalSse("link-github-issue", result.task.id);
23832
+ return json2({
23833
+ ok: true,
23834
+ task: result.task,
23835
+ created: result.created,
23836
+ moved: result.moved,
23837
+ generation
23838
+ });
23839
+ }
23840
+ if (action === "add-task") {
23841
+ const task = await updateJournalTaskState(cwd, (state) => {
23842
+ const result = addJournalTask(state, {
23843
+ title: bodyString(body, "title") || "",
23844
+ body: bodyString(body, "body"),
23845
+ status: bodyTaskStatus(body, "status"),
23846
+ priority: bodyTaskPriority(body, "priority"),
23847
+ labels: body.labels,
23848
+ due_date: bodyString(body, "due_date"),
23849
+ source_date: bodyString(body, "source_date"),
23850
+ journal_entry_id: bodyString(body, "journal_entry_id"),
23851
+ before_id: bodyString(body, "before_id"),
23852
+ after_id: bodyString(body, "after_id"),
23853
+ position: bodyNumber(body, "position")
23854
+ }, now);
23855
+ if (result.ok === false)
23856
+ throw new JournalRequestError(result.error);
23857
+ return { state: result.state, result: result.task };
23858
+ });
23859
+ journalSse("add-task", task.id);
23860
+ return json2({ ok: true, task, generation });
23861
+ }
23862
+ if (action === "update-task") {
23863
+ const id = bodyString(body, "id") || "";
23864
+ if (!id)
23865
+ return text("invalid id", 400);
23866
+ const task = await updateJournalTaskState(cwd, (state) => {
23867
+ const result = updateJournalTask(state, id, {
23868
+ title: bodyString(body, "title"),
23869
+ body: bodyString(body, "body"),
23870
+ status: bodyTaskStatus(body, "status"),
23871
+ priority: bodyTaskPriority(body, "priority"),
23872
+ labels: body.labels,
23873
+ due_date: body.due_date === null ? null : bodyString(body, "due_date"),
23874
+ source_date: body.source_date === null ? null : bodyString(body, "source_date"),
23875
+ journal_entry_id: body.journal_entry_id === null ? null : bodyString(body, "journal_entry_id")
23876
+ }, now);
23877
+ if (result.ok === false)
23878
+ throw new JournalRequestError(result.error);
23879
+ return { state: result.state, result: result.task };
23880
+ });
23881
+ journalSse("update-task", task.id);
23882
+ return json2({ ok: true, task, generation });
23883
+ }
23884
+ if (action === "move-task") {
23885
+ const id = bodyString(body, "id") || "";
23886
+ if (!id)
23887
+ return text("invalid id", 400);
23888
+ const task = await updateJournalTaskState(cwd, (state) => {
23889
+ const result = moveJournalTask(state, id, {
23890
+ status: bodyTaskStatus(body, "status"),
23891
+ before_id: bodyString(body, "before_id"),
23892
+ after_id: bodyString(body, "after_id"),
23893
+ position: bodyNumber(body, "position")
23894
+ }, now);
23895
+ if (result.ok === false)
23896
+ throw new JournalRequestError(result.error);
23897
+ return { state: result.state, result: result.task };
23898
+ });
23899
+ journalSse("move-task", task.id);
23900
+ return json2({ ok: true, task, generation });
23901
+ }
23902
+ if (action === "claim-task") {
23903
+ const id = bodyString(body, "id") || "";
23904
+ if (!id)
23905
+ return text("invalid id", 400);
23906
+ const task = await updateJournalTaskState(cwd, (state) => {
23907
+ const result = claimJournalTask(state, id, {
23908
+ by: bodyString(body, "by"),
23909
+ lease_minutes: bodyNumber(body, "lease_minutes"),
23910
+ wip_limit: bodyNumber(body, "wip_limit")
23911
+ }, now);
23912
+ if (result.ok === false)
23913
+ throw new JournalRequestError(result.error);
23914
+ return { state: result.state, result: result.task };
23915
+ });
23916
+ journalSse("claim-task", task.id);
23917
+ return json2({ ok: true, task, generation });
23918
+ }
23919
+ if (action === "complete-task") {
23920
+ const id = bodyString(body, "id") || "";
23921
+ if (!id)
23922
+ return text("invalid id", 400);
23923
+ const task = await updateJournalTaskState(cwd, (state) => {
23924
+ const result = completeJournalTask(state, id, {
23925
+ by: bodyString(body, "by"),
23926
+ note: bodyString(body, "note"),
23927
+ source: body.source === "user" ? "user" : "ai"
23928
+ }, now);
23929
+ if (result.ok === false)
23930
+ throw new JournalRequestError(result.error);
23931
+ return { state: result.state, result: result.task };
23932
+ });
23933
+ journalSse("complete-task", task.id);
23934
+ return json2({ ok: true, task, generation });
23935
+ }
23936
+ if (action === "delete-task") {
23937
+ const id = bodyString(body, "id") || "";
23938
+ if (!id)
23939
+ return text("invalid id", 400);
23940
+ const removed = await updateJournalTaskState(cwd, (state) => {
23941
+ const result = deleteJournalTask(state, id);
23942
+ return { state: result.state, result: result.removed };
23943
+ });
23944
+ if (removed)
23945
+ journalSse("delete-task", id);
23946
+ return json2({ ok: true, removed, generation });
23947
+ }
23948
+ } catch (error) {
23949
+ if (error instanceof GithubIssueListError) {
23950
+ return text(error.message, error.status);
23951
+ }
23952
+ if (error instanceof JournalRequestError) {
23953
+ return text(error.message, error.status);
23954
+ }
23955
+ throw error;
23956
+ }
23957
+ return text("invalid action", 400);
23958
+ }
21782
23959
  async function handleAnnotations(req) {
21783
23960
  if (req.method === "GET")
21784
23961
  return json2(await loadAnnotationsState(cwd));
@@ -21995,8 +24172,9 @@ function restartWorktreeWatch() {
21995
24172
  }
21996
24173
  worktreeWatch = startScopedWorktreeWatch();
21997
24174
  }
21998
- var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, commandOverrides, cwdWasExplicit = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, safePath, MCP_MAX_BODY_BYTES = 1048576, MCP_INSTRUCTIONS, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
24175
+ var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, commandOverrides, cwdWasExplicit = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, safePath, MCP_MAX_BODY_BYTES = 1048576, MCP_INSTRUCTIONS, JournalRequestError, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
21999
24176
  var init_preview = __esm(async () => {
24177
+ init_journal();
22000
24178
  init_routes();
22001
24179
  init_annotations();
22002
24180
  init_cache();
@@ -22004,6 +24182,8 @@ var init_preview = __esm(async () => {
22004
24182
  init_dev_assets();
22005
24183
  init_doctor();
22006
24184
  init_git();
24185
+ init_github_issues();
24186
+ init_journal2();
22007
24187
  init_mcp();
22008
24188
  init_raw_file_headers();
22009
24189
  init_root();
@@ -22013,8 +24193,8 @@ var init_preview = __esm(async () => {
22013
24193
  init_server_registry();
22014
24194
  init_state_store();
22015
24195
  init_worktree_watcher();
22016
- WEB_ROOT = join18(ROOT, "web");
22017
- VERSION = JSON.parse(readFileSync8(join18(ROOT, "package.json"), "utf8")).version;
24196
+ WEB_ROOT = join19(ROOT, "web");
24197
+ VERSION = JSON.parse(readFileSync9(join19(ROOT, "package.json"), "utf8")).version;
22018
24198
  DEFAULT_ARGS = ["HEAD"];
22019
24199
  WATCHED_ASSET_FILES = ["index.html", "style.css", "app.js"];
22020
24200
  LINE_INDEX_MAX_FILE_BYTES = 256 * 1024 * 1024;
@@ -22076,6 +24256,13 @@ var init_preview = __esm(async () => {
22076
24256
  blobBytesCache = new Map;
22077
24257
  safePath = isSafePath;
22078
24258
  MCP_INSTRUCTIONS = buildMcpInstructions();
24259
+ JournalRequestError = class JournalRequestError extends Error {
24260
+ status;
24261
+ constructor(message, status = 400) {
24262
+ super(message);
24263
+ this.status = status;
24264
+ }
24265
+ };
22079
24266
  isCodeViewerInternalPath = isToolInternalPath;
22080
24267
  parseCli();
22081
24268
  applyPersistedSettings(await loadAppSettingsState(cwd));
@@ -22141,6 +24328,8 @@ var init_preview = __esm(async () => {
22141
24328
  }
22142
24329
  if (url.pathname === "/_mcp")
22143
24330
  return handleMcp(req);
24331
+ if (url.pathname === "/_journal")
24332
+ return handleJournal(req);
22144
24333
  if (url.pathname === "/_annotations")
22145
24334
  return handleAnnotations(req);
22146
24335
  if (url.pathname === "/_refs") {
@@ -22271,6 +24460,9 @@ if (process.argv[2] === "agent-help") {
22271
24460
  } else if (process.argv[2] === "annotate") {
22272
24461
  const { runAnnotateCli: runAnnotateCli2 } = await Promise.resolve().then(() => (init_annotate_cli(), exports_annotate_cli));
22273
24462
  await runAnnotateCli2(process.argv.slice(3));
24463
+ } else if (process.argv[2] === "journal") {
24464
+ const { runJournalCli: runJournalCli2 } = await Promise.resolve().then(() => (init_journal_cli(), exports_journal_cli));
24465
+ await runJournalCli2(process.argv.slice(3));
22274
24466
  } else if (process.argv[2] === "query") {
22275
24467
  const sub = process.argv[3];
22276
24468
  const helpOnly = !sub || sub === "help" || sub === "agent-help" || sub === "--help" || sub === "-h";