open-walnut 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +339 -0
- package/bin/open-walnut.js +2 -0
- package/bin/walnut.js +2 -0
- package/dist/cli.js +137849 -0
- package/dist/daemon-binaries/acp-worker.js +44 -0
- package/dist/daemon-binaries/acp-worker.js.version +1 -0
- package/dist/data/skills/daily-standup/SKILL.md +16 -0
- package/dist/data/skills/install-plugin/SKILL.md +118 -0
- package/dist/data/skills/learn/SKILL.md +46 -0
- package/dist/data/skills/register-repo/SKILL.md +20 -0
- package/dist/data/skills/setup-cloud-companion/SKILL.md +326 -0
- package/dist/data/skills/triage/SKILL.md +23 -0
- package/dist/data/skills/walnut-calendar/SKILL.md +92 -0
- package/dist/data/skills/walnut-manage-repos/SKILL.md +107 -0
- package/dist/data/skills/walnut-peer-sessions/SKILL.md +61 -0
- package/dist/data/skills/walnut-register-repo/SKILL.md +72 -0
- package/dist/data/skills/walnut-register-repo/references/yaml-format.md +111 -0
- package/dist/data/skills/walnut-register-repo/scripts/validate-repo.py +177 -0
- package/dist/data/skills/walnut-routines/SKILL.md +65 -0
- package/dist/data/skills/weekly-review/SKILL.md +22 -0
- package/dist/data/walnut-calendar.swift +213 -0
- package/dist/data/walnut-extract.swift +80 -0
- package/dist/hooks/on-compact.js +976 -0
- package/dist/hooks/on-stop.js +1329 -0
- package/dist/integrations/jira/index.js +4200 -0
- package/dist/integrations/jira/manifest.json +26 -0
- package/dist/integrations/local/index.js +51 -0
- package/dist/integrations/local/manifest.json +10 -0
- package/dist/integrations/ms-todo/index.js +4672 -0
- package/dist/integrations/ms-todo/manifest.json +17 -0
- package/dist/session-server/index.js +1382 -0
- package/dist/web/server.js +135275 -0
- package/dist/web/static/assets/apl-B4CMkyY2.js +1 -0
- package/dist/web/static/assets/asciiarmor-Df11BRmG.js +1 -0
- package/dist/web/static/assets/asn1-EdZsLKOL.js +1 -0
- package/dist/web/static/assets/asterisk-B-8jnY81.js +1 -0
- package/dist/web/static/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/dist/web/static/assets/clike-B9uivgTg.js +1 -0
- package/dist/web/static/assets/clojure-BMjYHr_A.js +1 -0
- package/dist/web/static/assets/cmake-BQqOBYOt.js +1 -0
- package/dist/web/static/assets/cobol-CWcv1MsR.js +1 -0
- package/dist/web/static/assets/coffeescript-S37ZYGWr.js +1 -0
- package/dist/web/static/assets/commonlisp-DBKNyK5s.js +1 -0
- package/dist/web/static/assets/crystal-SjHAIU92.js +1 -0
- package/dist/web/static/assets/css-BnMrqG3P.js +1 -0
- package/dist/web/static/assets/cypher-C_CwsFkJ.js +1 -0
- package/dist/web/static/assets/d-pRatUO7H.js +1 -0
- package/dist/web/static/assets/diff-DbItnlRl.js +1 -0
- package/dist/web/static/assets/dockerfile-BKs6k2Af.js +1 -0
- package/dist/web/static/assets/dtd-DF_7sFjM.js +1 -0
- package/dist/web/static/assets/dylan-DwRh75JA.js +1 -0
- package/dist/web/static/assets/ebnf-CDyGwa7X.js +1 -0
- package/dist/web/static/assets/ecl-Cabwm37j.js +1 -0
- package/dist/web/static/assets/eiffel-CnydiIhH.js +1 -0
- package/dist/web/static/assets/elm-vLlmbW-K.js +1 -0
- package/dist/web/static/assets/erlang-BNw1qcRV.js +1 -0
- package/dist/web/static/assets/factor-kuTfRLto.js +1 -0
- package/dist/web/static/assets/fcl-Kvtd6kyn.js +1 -0
- package/dist/web/static/assets/forth-Ffai-XNe.js +1 -0
- package/dist/web/static/assets/fortran-DYz_wnZ1.js +1 -0
- package/dist/web/static/assets/gas-Bneqetm1.js +1 -0
- package/dist/web/static/assets/gherkin-heZmZLOM.js +1 -0
- package/dist/web/static/assets/groovy-D9Dt4D0W.js +1 -0
- package/dist/web/static/assets/haskell-Cw1EW3IL.js +1 -0
- package/dist/web/static/assets/haxe-H-WmDvRZ.js +1 -0
- package/dist/web/static/assets/http-DBlCnlav.js +1 -0
- package/dist/web/static/assets/idl-BEugSyMb.js +1 -0
- package/dist/web/static/assets/index-B3K5yAdA.js +1 -0
- package/dist/web/static/assets/index-B7OvpXOs.js +632 -0
- package/dist/web/static/assets/index-C07UNJIJ.js +1 -0
- package/dist/web/static/assets/index-C8k-3wfu.js +2 -0
- package/dist/web/static/assets/index-CBXGsT3t.js +1 -0
- package/dist/web/static/assets/index-CUrO3qpX.js +1 -0
- package/dist/web/static/assets/index-CpTvBBAa.js +1 -0
- package/dist/web/static/assets/index-D4J6slTf.js +1 -0
- package/dist/web/static/assets/index-DUgC5K_q.js +3 -0
- package/dist/web/static/assets/index-DhqXE1wa.js +1 -0
- package/dist/web/static/assets/index-DkGqHTCk.js +1 -0
- package/dist/web/static/assets/index-DsxFiBic.js +1 -0
- package/dist/web/static/assets/index-Dz3umkfm.js +1 -0
- package/dist/web/static/assets/index-G2v0oaqp.js +1 -0
- package/dist/web/static/assets/index-GNDKJKwy.js +1 -0
- package/dist/web/static/assets/index-U1CVtNUH.css +32 -0
- package/dist/web/static/assets/index-ft4kpQXq.js +1 -0
- package/dist/web/static/assets/index-ls7h0u-p.js +1 -0
- package/dist/web/static/assets/index-nL5l5WPW.js +7 -0
- package/dist/web/static/assets/javascript-iXu5QeM3.js +1 -0
- package/dist/web/static/assets/julia-DuME0IfC.js +1 -0
- package/dist/web/static/assets/livescript-BwQOo05w.js +1 -0
- package/dist/web/static/assets/lua-VAEuO923.js +1 -0
- package/dist/web/static/assets/mathematica-DTrFuWx2.js +1 -0
- package/dist/web/static/assets/mbox-CNhZ1qSd.js +1 -0
- package/dist/web/static/assets/mirc-CjQqDB4T.js +1 -0
- package/dist/web/static/assets/mllike-CXdrOF99.js +1 -0
- package/dist/web/static/assets/modelica-Dc1JOy9r.js +1 -0
- package/dist/web/static/assets/mscgen-BA5vi2Kp.js +1 -0
- package/dist/web/static/assets/mumps-BT43cFF4.js +1 -0
- package/dist/web/static/assets/nginx-DdIZxoE0.js +1 -0
- package/dist/web/static/assets/nsis-LdVXkNf5.js +1 -0
- package/dist/web/static/assets/ntriples-BfvgReVJ.js +1 -0
- package/dist/web/static/assets/octave-Ck1zUtKM.js +1 -0
- package/dist/web/static/assets/oz-BzwKVEFT.js +1 -0
- package/dist/web/static/assets/pascal--L3eBynH.js +1 -0
- package/dist/web/static/assets/perl-CdXCOZ3F.js +1 -0
- package/dist/web/static/assets/pig-CevX1Tat.js +1 -0
- package/dist/web/static/assets/powershell-CFHJl5sT.js +1 -0
- package/dist/web/static/assets/properties-C78fOPTZ.js +1 -0
- package/dist/web/static/assets/protobuf-ChK-085T.js +1 -0
- package/dist/web/static/assets/pug-DeIclll2.js +1 -0
- package/dist/web/static/assets/puppet-DMA9R1ak.js +1 -0
- package/dist/web/static/assets/python-BuPzkPfP.js +1 -0
- package/dist/web/static/assets/q-pXgVlZs6.js +1 -0
- package/dist/web/static/assets/r-B6wPVr8A.js +1 -0
- package/dist/web/static/assets/rpm-CTu-6PCP.js +1 -0
- package/dist/web/static/assets/ruby-B2Rjki9n.js +1 -0
- package/dist/web/static/assets/sas-B4kiWyti.js +1 -0
- package/dist/web/static/assets/scheme-C41bIUwD.js +1 -0
- package/dist/web/static/assets/shell-CjFT_Tl9.js +1 -0
- package/dist/web/static/assets/sieve-C3Gn_uJK.js +1 -0
- package/dist/web/static/assets/simple-mode-GW_nhZxv.js +1 -0
- package/dist/web/static/assets/smalltalk-CnHTOXQT.js +1 -0
- package/dist/web/static/assets/solr-DehyRSwq.js +1 -0
- package/dist/web/static/assets/sparql-DkYu6x3z.js +1 -0
- package/dist/web/static/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/dist/web/static/assets/sql-D0XecflT.js +1 -0
- package/dist/web/static/assets/stex-C3f8Ysf7.js +1 -0
- package/dist/web/static/assets/stylus-B533Al4x.js +1 -0
- package/dist/web/static/assets/swift-BzpIVaGY.js +1 -0
- package/dist/web/static/assets/tcl-DVfN8rqt.js +1 -0
- package/dist/web/static/assets/textile-CnDTJFAw.js +1 -0
- package/dist/web/static/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/dist/web/static/assets/tiki-DGYXhP31.js +1 -0
- package/dist/web/static/assets/toml-Bm5Em-hy.js +1 -0
- package/dist/web/static/assets/troff-wAsdV37c.js +1 -0
- package/dist/web/static/assets/ttcn-CfJYG6tj.js +1 -0
- package/dist/web/static/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/dist/web/static/assets/turtle-B1tBg_DP.js +1 -0
- package/dist/web/static/assets/vb-CmGdzxic.js +1 -0
- package/dist/web/static/assets/vbscript-BuJXcnF6.js +1 -0
- package/dist/web/static/assets/velocity-D8B20fx6.js +1 -0
- package/dist/web/static/assets/verilog-C6RDOZhf.js +1 -0
- package/dist/web/static/assets/vhdl-lSbBsy5d.js +1 -0
- package/dist/web/static/assets/webidl-ZXfAyPTL.js +1 -0
- package/dist/web/static/assets/xquery-DzFWVndE.js +1 -0
- package/dist/web/static/assets/yacas-BJ4BC0dw.js +1 -0
- package/dist/web/static/assets/z80-Hz9HOZM7.js +1 -0
- package/dist/web/static/index.html +22 -0
- package/dist/web/static/walnut-icon.png +0 -0
- package/dist/workers/git-compaction-worker.js +898 -0
- package/dist/workers/qmd-index-worker.js +97162 -0
- package/package.json +143 -0
- package/patches/@anthropic-ai+claude-agent-sdk+0.2.50.patch +13 -0
- package/scripts/patch-qmd.mjs +171 -0
- package/scripts/postinstall.mjs +120 -0
|
@@ -0,0 +1,4200 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/constants.ts
|
|
12
|
+
import path from "path";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import os from "os";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
function resolveOpenWalnutHome() {
|
|
17
|
+
const envHome = process.env.OPEN_WALNUT_HOME;
|
|
18
|
+
const productionHome = path.join(os.homedir(), ".open-walnut");
|
|
19
|
+
const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
|
|
20
|
+
if (isTestEnv) {
|
|
21
|
+
if (envHome && envHome !== productionHome && !envHome.startsWith(productionHome + path.sep)) {
|
|
22
|
+
assertNotProductionPath(envHome);
|
|
23
|
+
return envHome;
|
|
24
|
+
}
|
|
25
|
+
const testHome = path.join(os.tmpdir(), `open-walnut-test-${process.pid}`);
|
|
26
|
+
process.env.OPEN_WALNUT_HOME = testHome;
|
|
27
|
+
return testHome;
|
|
28
|
+
}
|
|
29
|
+
if (!envHome) return productionHome;
|
|
30
|
+
if (process.argv.includes("--_ephemeral-child")) return envHome;
|
|
31
|
+
if (isEphemeralTmpDir(envHome)) {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
`WARNING: OPEN_WALNUT_HOME=${envHome} looks like a leaked ephemeral temp dir.
|
|
34
|
+
Overriding to ${productionHome}. (Only --_ephemeral-child processes may use one.)
|
|
35
|
+
`
|
|
36
|
+
);
|
|
37
|
+
process.env.OPEN_WALNUT_HOME = productionHome;
|
|
38
|
+
return productionHome;
|
|
39
|
+
}
|
|
40
|
+
return envHome;
|
|
41
|
+
}
|
|
42
|
+
function assertNotProductionPath(inputPath) {
|
|
43
|
+
const isTestEnv = !!(process.env.VITEST || process.env.VITEST_WORKER_ID || process.env.NODE_ENV === "test");
|
|
44
|
+
if (!isTestEnv) return;
|
|
45
|
+
const resolved = path.resolve(inputPath);
|
|
46
|
+
const prodHome = path.join(os.homedir(), ".open-walnut");
|
|
47
|
+
if (resolved === prodHome || resolved.startsWith(prodHome + path.sep)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`SAFETY: Test process attempted to use production path: ${resolved}
|
|
50
|
+
This would destroy real user data in ~/.open-walnut/.
|
|
51
|
+
Set OPEN_WALNUT_HOME to a temp directory or let constants.ts auto-assign one.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isEphemeralTmpDir(inputPath) {
|
|
56
|
+
if (!/[\\/]open-walnut-[^\\/]+$/.test(inputPath)) return false;
|
|
57
|
+
const tmpDirs = /* @__PURE__ */ new Set();
|
|
58
|
+
tmpDirs.add(os.tmpdir());
|
|
59
|
+
try {
|
|
60
|
+
tmpDirs.add(fs.realpathSync(os.tmpdir()));
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
tmpDirs.add(fs.realpathSync("/tmp"));
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
const parent = path.dirname(path.resolve(inputPath));
|
|
68
|
+
if (tmpDirs.has(parent)) return true;
|
|
69
|
+
try {
|
|
70
|
+
return tmpDirs.has(fs.realpathSync(parent));
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function agentMemoryDir(agentId) {
|
|
76
|
+
if (!agentId || agentId === "general") return WALNUT_HOME;
|
|
77
|
+
return path.join(MEMORY_DIR, "agents", agentId);
|
|
78
|
+
}
|
|
79
|
+
var WALNUT_HOME, IS_EPHEMERAL, CLOUD_MODE, TASKS_DIR, TASKS_FILE, ARCHIVE_DIR, MEMORY_DIR, SESSIONS_DIR, PROJECTS_DIR, CONFIG_FILE, SYNC_DIR, PLUGIN_STORES_DIR, SESSIONS_FILE, CLAUDE_HOME, HISTORY_CACHE_DIR, PROJECTION_CACHE_DIR, TRANSCRIPT_CACHE_DIR, TASK_QUEUE_DIR, HOST_MODEL_CATALOG_FILE, HOOK_LOG_FILE, DAILY_DIR, MEMORY_FILE, USER_FILE, LEGACY_MEMORY_FILE, PROJECTS_MEMORY_DIR, CHAT_HISTORY_FILE, CONVERSATIONS_DIR, GLOBAL_SKILLS_DIR, SKILL_SETTINGS_FILE, CLAUDE_SKILLS_DIR, CLAUDE_SETTINGS_FILE, CLAUDE_PLUGINS_DIR, CLAUDE_CREDENTIALS_FILE, CRON_FILE, USAGE_DB_FILE, LOG_DIR, SESSION_STREAMS_DIR, DTACH_SOCKET_DIR, SESSION_QUEUE_FILE, IMAGES_DIR, REMOTE_IMAGES_DIR, PASTES_DIR, HEARTBEAT_FILE, COMMANDS_DIR, BUILTIN_COMMANDS_DIR, BUILTIN_SKILLS_DIR, FREQUENT_DIRS_FILE, MENTION_DIRS_FILE, NOTES_DIR, GLOBAL_NOTES_FILE, NOTES_AGENTS_FILE, NOTES_CLAUDE_FILE, REPOSITORIES_DIR, REPOS_MEMORY_DIR, TOPICS_DIR, COMPACTION_DIR, MEMORY_INDEX_FILE, WORKING_MEMORY_FILE, TIMELINE_DIR, CONFIG_SHARE_DIR, UI_PREFS_FILE, STT_VOCAB_FILE, TMP_DIR, RECORDINGS_DIR, LOG_PREFIX, DAEMON_BINARIES_DIR, WALNUT_INSTALL_DIR;
|
|
80
|
+
var init_constants = __esm({
|
|
81
|
+
"src/constants.ts"() {
|
|
82
|
+
"use strict";
|
|
83
|
+
WALNUT_HOME = resolveOpenWalnutHome();
|
|
84
|
+
IS_EPHEMERAL = process.argv.includes("--_ephemeral-child");
|
|
85
|
+
CLOUD_MODE = process.env.WALNUT_CLOUD_MODE === "1";
|
|
86
|
+
TASKS_DIR = path.join(WALNUT_HOME, "tasks");
|
|
87
|
+
TASKS_FILE = path.join(TASKS_DIR, "tasks.json");
|
|
88
|
+
ARCHIVE_DIR = path.join(TASKS_DIR, "archive");
|
|
89
|
+
MEMORY_DIR = path.join(WALNUT_HOME, "memory");
|
|
90
|
+
SESSIONS_DIR = path.join(MEMORY_DIR, "sessions");
|
|
91
|
+
PROJECTS_DIR = path.join(MEMORY_DIR, "projects");
|
|
92
|
+
CONFIG_FILE = path.join(WALNUT_HOME, "config.yaml");
|
|
93
|
+
SYNC_DIR = path.join(WALNUT_HOME, "sync");
|
|
94
|
+
PLUGIN_STORES_DIR = path.join(WALNUT_HOME, "plugin-stores");
|
|
95
|
+
SESSIONS_FILE = path.join(WALNUT_HOME, "sessions.json");
|
|
96
|
+
CLAUDE_HOME = path.join(os.homedir(), ".claude");
|
|
97
|
+
HISTORY_CACHE_DIR = path.join(WALNUT_HOME, "cache", "history");
|
|
98
|
+
PROJECTION_CACHE_DIR = path.join(WALNUT_HOME, "cache", "projections");
|
|
99
|
+
TRANSCRIPT_CACHE_DIR = path.join(WALNUT_HOME, "cache", "transcripts");
|
|
100
|
+
TASK_QUEUE_DIR = path.join(WALNUT_HOME, "cache", "task-queue");
|
|
101
|
+
HOST_MODEL_CATALOG_FILE = path.join(WALNUT_HOME, "cache", "host-model-catalogs.json");
|
|
102
|
+
HOOK_LOG_FILE = path.join(WALNUT_HOME, "hook-errors.log");
|
|
103
|
+
DAILY_DIR = path.join(MEMORY_DIR, "daily");
|
|
104
|
+
MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
|
|
105
|
+
USER_FILE = path.join(MEMORY_DIR, "USER.md");
|
|
106
|
+
LEGACY_MEMORY_FILE = path.join(WALNUT_HOME, "MEMORY.md");
|
|
107
|
+
PROJECTS_MEMORY_DIR = path.join(MEMORY_DIR, "projects");
|
|
108
|
+
CHAT_HISTORY_FILE = path.join(WALNUT_HOME, "chat-history.json");
|
|
109
|
+
CONVERSATIONS_DIR = path.join(WALNUT_HOME, "conversations");
|
|
110
|
+
GLOBAL_SKILLS_DIR = path.join(WALNUT_HOME, "skills");
|
|
111
|
+
SKILL_SETTINGS_FILE = path.join(WALNUT_HOME, "skill-settings.json");
|
|
112
|
+
CLAUDE_SKILLS_DIR = path.join(CLAUDE_HOME, "skills");
|
|
113
|
+
CLAUDE_SETTINGS_FILE = path.join(CLAUDE_HOME, "settings.json");
|
|
114
|
+
CLAUDE_PLUGINS_DIR = path.join(CLAUDE_HOME, "plugins");
|
|
115
|
+
CLAUDE_CREDENTIALS_FILE = path.join(CLAUDE_HOME, ".credentials.json");
|
|
116
|
+
CRON_FILE = path.join(WALNUT_HOME, "cron-jobs.json");
|
|
117
|
+
USAGE_DB_FILE = path.join(WALNUT_HOME, "usage.sqlite");
|
|
118
|
+
LOG_DIR = process.env.WALNUT_DAEMON_DIR || "/tmp/open-walnut";
|
|
119
|
+
SESSION_STREAMS_DIR = path.join(LOG_DIR, "streams");
|
|
120
|
+
DTACH_SOCKET_DIR = path.join(LOG_DIR, "term");
|
|
121
|
+
SESSION_QUEUE_FILE = path.join(WALNUT_HOME, "session-message-queue.json");
|
|
122
|
+
IMAGES_DIR = path.join(LOG_DIR, "images");
|
|
123
|
+
REMOTE_IMAGES_DIR = path.join(IMAGES_DIR, "remote");
|
|
124
|
+
PASTES_DIR = path.join(LOG_DIR, "pastes");
|
|
125
|
+
HEARTBEAT_FILE = path.join(WALNUT_HOME, "HEARTBEAT.md");
|
|
126
|
+
COMMANDS_DIR = path.join(WALNUT_HOME, "commands");
|
|
127
|
+
BUILTIN_COMMANDS_DIR = (() => {
|
|
128
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
129
|
+
for (let i = 0; i < 5; i++) {
|
|
130
|
+
const candidate = path.join(dir, "data", "slash-commands");
|
|
131
|
+
try {
|
|
132
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
dir = path.dirname(dir);
|
|
136
|
+
}
|
|
137
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "slash-commands");
|
|
138
|
+
})();
|
|
139
|
+
BUILTIN_SKILLS_DIR = (() => {
|
|
140
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
141
|
+
for (let i = 0; i < 5; i++) {
|
|
142
|
+
const candidate = path.join(dir, "data", "skills");
|
|
143
|
+
try {
|
|
144
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
dir = path.dirname(dir);
|
|
148
|
+
}
|
|
149
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "data", "skills");
|
|
150
|
+
})();
|
|
151
|
+
FREQUENT_DIRS_FILE = path.join(WALNUT_HOME, "frequent-directories.json");
|
|
152
|
+
MENTION_DIRS_FILE = path.join(WALNUT_HOME, "mention-directories.json");
|
|
153
|
+
NOTES_DIR = path.join(WALNUT_HOME, "notes");
|
|
154
|
+
GLOBAL_NOTES_FILE = path.join(NOTES_DIR, "global-notes.md");
|
|
155
|
+
NOTES_AGENTS_FILE = path.join(NOTES_DIR, "AGENTS.md");
|
|
156
|
+
NOTES_CLAUDE_FILE = path.join(NOTES_DIR, "CLAUDE.md");
|
|
157
|
+
REPOSITORIES_DIR = path.join(WALNUT_HOME, "repositories");
|
|
158
|
+
REPOS_MEMORY_DIR = path.join(GLOBAL_SKILLS_DIR, "repos");
|
|
159
|
+
TOPICS_DIR = path.join(MEMORY_DIR, "topics");
|
|
160
|
+
COMPACTION_DIR = path.join(MEMORY_DIR, "compaction");
|
|
161
|
+
MEMORY_INDEX_FILE = path.join(MEMORY_DIR, "index.md");
|
|
162
|
+
WORKING_MEMORY_FILE = path.join(MEMORY_DIR, "working-memory.md");
|
|
163
|
+
TIMELINE_DIR = path.join(WALNUT_HOME, "timeline");
|
|
164
|
+
CONFIG_SHARE_DIR = path.join(WALNUT_HOME, "config", "share");
|
|
165
|
+
UI_PREFS_FILE = path.join(CONFIG_SHARE_DIR, "ui-prefs.json");
|
|
166
|
+
STT_VOCAB_FILE = path.join(CONFIG_SHARE_DIR, "stt-vocab.txt");
|
|
167
|
+
TMP_DIR = path.join(WALNUT_HOME, "tmp");
|
|
168
|
+
RECORDINGS_DIR = path.join(TMP_DIR, "recordings");
|
|
169
|
+
LOG_PREFIX = "open-walnut-";
|
|
170
|
+
DAEMON_BINARIES_DIR = (() => {
|
|
171
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
172
|
+
for (let i = 0; i < 5; i++) {
|
|
173
|
+
const candidate = path.join(dir, "dist", "daemon-binaries");
|
|
174
|
+
try {
|
|
175
|
+
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
dir = path.dirname(dir);
|
|
179
|
+
}
|
|
180
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist", "daemon-binaries");
|
|
181
|
+
})();
|
|
182
|
+
WALNUT_INSTALL_DIR = (() => {
|
|
183
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
184
|
+
for (let i = 0; i < 6; i++) {
|
|
185
|
+
try {
|
|
186
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
187
|
+
if (pkg.name === "open-walnut" && fs.statSync(path.join(dir, ".git")).isDirectory()) return dir;
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
const parent = path.dirname(dir);
|
|
191
|
+
if (parent === dir) break;
|
|
192
|
+
dir = parent;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
})();
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/logging/redact.ts
|
|
200
|
+
function redactSensitiveText(text) {
|
|
201
|
+
let result = text;
|
|
202
|
+
for (const { re, replacement } of patterns) {
|
|
203
|
+
result = result.replace(re, replacement);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
var REDACTED, patterns;
|
|
208
|
+
var init_redact = __esm({
|
|
209
|
+
"src/logging/redact.ts"() {
|
|
210
|
+
"use strict";
|
|
211
|
+
REDACTED = "[REDACTED]";
|
|
212
|
+
patterns = [
|
|
213
|
+
// PEM private key blocks (multiline)
|
|
214
|
+
{
|
|
215
|
+
re: /-----BEGIN\s[\w\s]*PRIVATE KEY-----[\s\S]*?-----END\s[\w\s]*PRIVATE KEY-----/g,
|
|
216
|
+
replacement: REDACTED
|
|
217
|
+
},
|
|
218
|
+
// Bearer tokens: Authorization: Bearer <token>
|
|
219
|
+
{
|
|
220
|
+
re: /(Bearer\s+)\S+/gi,
|
|
221
|
+
replacement: `$1${REDACTED}`
|
|
222
|
+
},
|
|
223
|
+
// OpenAI / Anthropic style keys: sk-... (at least 20 chars after prefix)
|
|
224
|
+
{
|
|
225
|
+
re: /\bsk-[A-Za-z0-9_-]{20,}\b/g,
|
|
226
|
+
replacement: REDACTED
|
|
227
|
+
},
|
|
228
|
+
// AWS access key IDs: AKIA...
|
|
229
|
+
{
|
|
230
|
+
re: /\bAKIA[A-Z0-9]{12,}\b/g,
|
|
231
|
+
replacement: REDACTED
|
|
232
|
+
},
|
|
233
|
+
// AWS secret access key in key=value form
|
|
234
|
+
{
|
|
235
|
+
re: /(aws_secret_access_key\s*[=:]\s*)\S+/gi,
|
|
236
|
+
replacement: `$1${REDACTED}`
|
|
237
|
+
},
|
|
238
|
+
// AWS session token in key=value form
|
|
239
|
+
{
|
|
240
|
+
re: /(aws_session_token\s*[=:]\s*)\S+/gi,
|
|
241
|
+
replacement: `$1${REDACTED}`
|
|
242
|
+
},
|
|
243
|
+
// Credentials in URL userinfo: https://user:secret@host/...
|
|
244
|
+
// Motivating shape: execSync embeds the whole command in error.message, so a
|
|
245
|
+
// failed `git remote add origin https://walnut:<device-token>@host/git/data`
|
|
246
|
+
// otherwise logs the cloud companion's long-lived credential verbatim.
|
|
247
|
+
{
|
|
248
|
+
re: /(https?:\/\/[^/\s:@]+:)[^@/\s]+@/gi,
|
|
249
|
+
replacement: `$1${REDACTED}@`
|
|
250
|
+
},
|
|
251
|
+
// Generic secrets: password=, secret=, token=, api_key=, apikey=
|
|
252
|
+
{
|
|
253
|
+
re: /((?:password|secret|token|api_key|apikey)\s*[=:]\s*)\S+/gi,
|
|
254
|
+
replacement: `$1${REDACTED}`
|
|
255
|
+
}
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// src/logging/logger.ts
|
|
261
|
+
import fs2 from "fs";
|
|
262
|
+
import fsp from "fs/promises";
|
|
263
|
+
import path2 from "path";
|
|
264
|
+
function ensureLogDir() {
|
|
265
|
+
if (dirEnsured) return;
|
|
266
|
+
try {
|
|
267
|
+
fs2.mkdirSync(LOG_DIR, { recursive: true });
|
|
268
|
+
dirEnsured = true;
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function todayFileName() {
|
|
273
|
+
const d = /* @__PURE__ */ new Date();
|
|
274
|
+
const yyyy = d.getFullYear();
|
|
275
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
276
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
277
|
+
return `${LOG_PREFIX}${yyyy}-${mm}-${dd}.log`;
|
|
278
|
+
}
|
|
279
|
+
function logFilePath() {
|
|
280
|
+
return path2.join(LOG_DIR, todayFileName());
|
|
281
|
+
}
|
|
282
|
+
function startFlushTimer() {
|
|
283
|
+
if (flushTimer) return;
|
|
284
|
+
flushTimer = setInterval(() => {
|
|
285
|
+
void flushLogBuffer();
|
|
286
|
+
}, FLUSH_INTERVAL_MS);
|
|
287
|
+
if (flushTimer && typeof flushTimer === "object" && "unref" in flushTimer) {
|
|
288
|
+
flushTimer.unref();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function flushLogBuffer() {
|
|
292
|
+
if (flushing || buffer.length === 0) return;
|
|
293
|
+
flushing = true;
|
|
294
|
+
const batch = buffer;
|
|
295
|
+
buffer = [];
|
|
296
|
+
try {
|
|
297
|
+
ensureLogDir();
|
|
298
|
+
await fsp.appendFile(logFilePath(), batch.join(""), "utf-8");
|
|
299
|
+
} catch {
|
|
300
|
+
} finally {
|
|
301
|
+
flushing = false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function flushLogBufferSync() {
|
|
305
|
+
if (buffer.length === 0) return;
|
|
306
|
+
const batch = buffer;
|
|
307
|
+
buffer = [];
|
|
308
|
+
try {
|
|
309
|
+
ensureLogDir();
|
|
310
|
+
fs2.appendFileSync(logFilePath(), batch.join(""), "utf-8");
|
|
311
|
+
} catch {
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function writeLogEntry(entry) {
|
|
315
|
+
const line = redactSensitiveText(JSON.stringify(entry)) + "\n";
|
|
316
|
+
buffer.push(line);
|
|
317
|
+
startFlushTimer();
|
|
318
|
+
}
|
|
319
|
+
var dirEnsured, FLUSH_INTERVAL_MS, buffer, flushTimer, flushing;
|
|
320
|
+
var init_logger = __esm({
|
|
321
|
+
"src/logging/logger.ts"() {
|
|
322
|
+
"use strict";
|
|
323
|
+
init_constants();
|
|
324
|
+
init_redact();
|
|
325
|
+
dirEnsured = false;
|
|
326
|
+
FLUSH_INTERVAL_MS = 2e3;
|
|
327
|
+
buffer = [];
|
|
328
|
+
flushTimer = null;
|
|
329
|
+
flushing = false;
|
|
330
|
+
process.on("beforeExit", flushLogBufferSync);
|
|
331
|
+
process.on("exit", flushLogBufferSync);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// src/logging/levels.ts
|
|
336
|
+
function resolveConfiguredLevel() {
|
|
337
|
+
const raw = process.env.WALNUT_LOG_LEVEL?.toLowerCase().trim();
|
|
338
|
+
if (raw && raw in LOG_LEVEL_ORDER) return raw;
|
|
339
|
+
return DEFAULT_LEVEL;
|
|
340
|
+
}
|
|
341
|
+
function shouldLog(messageLevel, configuredLevel = CONFIGURED_LOG_LEVEL) {
|
|
342
|
+
return LOG_LEVEL_ORDER[messageLevel] >= LOG_LEVEL_ORDER[configuredLevel];
|
|
343
|
+
}
|
|
344
|
+
var LOG_LEVEL_ORDER, DEFAULT_LEVEL, CONFIGURED_LOG_LEVEL;
|
|
345
|
+
var init_levels = __esm({
|
|
346
|
+
"src/logging/levels.ts"() {
|
|
347
|
+
"use strict";
|
|
348
|
+
LOG_LEVEL_ORDER = {
|
|
349
|
+
trace: 0,
|
|
350
|
+
debug: 1,
|
|
351
|
+
info: 2,
|
|
352
|
+
warn: 3,
|
|
353
|
+
error: 4,
|
|
354
|
+
fatal: 5
|
|
355
|
+
};
|
|
356
|
+
DEFAULT_LEVEL = "info";
|
|
357
|
+
CONFIGURED_LOG_LEVEL = resolveConfiguredLevel();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// src/logging/subsystem.ts
|
|
362
|
+
import chalk from "chalk";
|
|
363
|
+
function timeStamp(d) {
|
|
364
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
365
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
366
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
367
|
+
return `${hh}:${mm}:${ss}`;
|
|
368
|
+
}
|
|
369
|
+
function formatMeta(meta) {
|
|
370
|
+
if (!meta || Object.keys(meta).length === 0) return "";
|
|
371
|
+
return " " + chalk.dim(JSON.stringify(meta));
|
|
372
|
+
}
|
|
373
|
+
function forwardToErrorSink(subsystem, message, meta) {
|
|
374
|
+
const sink = globalThis[ERROR_SINK_KEY];
|
|
375
|
+
if (!sink || inErrorSink) return;
|
|
376
|
+
if (subsystem.startsWith("notif")) return;
|
|
377
|
+
inErrorSink = true;
|
|
378
|
+
try {
|
|
379
|
+
sink({ subsystem, message, meta });
|
|
380
|
+
} catch {
|
|
381
|
+
} finally {
|
|
382
|
+
inErrorSink = false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function emit(level, subsystem, message, meta) {
|
|
386
|
+
if (!shouldLog(level)) return;
|
|
387
|
+
const now = /* @__PURE__ */ new Date();
|
|
388
|
+
writeLogEntry({
|
|
389
|
+
time: now.toISOString(),
|
|
390
|
+
level,
|
|
391
|
+
subsystem,
|
|
392
|
+
message,
|
|
393
|
+
...meta && Object.keys(meta).length > 0 ? meta : {}
|
|
394
|
+
});
|
|
395
|
+
if (level === "error" || level === "fatal") {
|
|
396
|
+
forwardToErrorSink(subsystem, message, meta);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const colorFn = levelColor[level];
|
|
400
|
+
const tag = chalk.bold(`[${subsystem}]`);
|
|
401
|
+
const lbl = colorFn(levelLabel[level]);
|
|
402
|
+
const ts = chalk.dim(timeStamp(now));
|
|
403
|
+
const metaStr = formatMeta(meta);
|
|
404
|
+
const line = redactSensitiveText(`${ts} ${lbl} ${tag} ${message}${metaStr}
|
|
405
|
+
`);
|
|
406
|
+
process.stderr.write(line);
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function createSubsystemLogger(subsystem) {
|
|
411
|
+
const logger = {
|
|
412
|
+
trace: (msg, meta) => emit("trace", subsystem, msg, meta),
|
|
413
|
+
debug: (msg, meta) => emit("debug", subsystem, msg, meta),
|
|
414
|
+
info: (msg, meta) => emit("info", subsystem, msg, meta),
|
|
415
|
+
warn: (msg, meta) => emit("warn", subsystem, msg, meta),
|
|
416
|
+
error: (msg, meta) => emit("error", subsystem, msg, meta),
|
|
417
|
+
fatal: (msg, meta) => emit("fatal", subsystem, msg, meta),
|
|
418
|
+
child(subName) {
|
|
419
|
+
return createSubsystemLogger(`${subsystem}/${subName}`);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
return logger;
|
|
423
|
+
}
|
|
424
|
+
var levelColor, levelLabel, ERROR_SINK_KEY, inErrorSink;
|
|
425
|
+
var init_subsystem = __esm({
|
|
426
|
+
"src/logging/subsystem.ts"() {
|
|
427
|
+
"use strict";
|
|
428
|
+
init_levels();
|
|
429
|
+
init_logger();
|
|
430
|
+
init_redact();
|
|
431
|
+
levelColor = {
|
|
432
|
+
trace: chalk.gray,
|
|
433
|
+
debug: chalk.cyan,
|
|
434
|
+
info: chalk.green,
|
|
435
|
+
warn: chalk.yellow,
|
|
436
|
+
error: chalk.red,
|
|
437
|
+
fatal: chalk.bgRed.white
|
|
438
|
+
};
|
|
439
|
+
levelLabel = {
|
|
440
|
+
trace: "TRC",
|
|
441
|
+
debug: "DBG",
|
|
442
|
+
info: "INF",
|
|
443
|
+
warn: "WRN",
|
|
444
|
+
error: "ERR",
|
|
445
|
+
fatal: "FTL"
|
|
446
|
+
};
|
|
447
|
+
ERROR_SINK_KEY = /* @__PURE__ */ Symbol.for("open-walnut.errorNotificationSink");
|
|
448
|
+
inErrorSink = false;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// src/logging/index.ts
|
|
453
|
+
var log;
|
|
454
|
+
var init_logging = __esm({
|
|
455
|
+
"src/logging/index.ts"() {
|
|
456
|
+
"use strict";
|
|
457
|
+
init_logger();
|
|
458
|
+
init_subsystem();
|
|
459
|
+
init_subsystem();
|
|
460
|
+
init_levels();
|
|
461
|
+
init_redact();
|
|
462
|
+
log = {
|
|
463
|
+
bus: createSubsystemLogger("bus"),
|
|
464
|
+
agent: createSubsystemLogger("agent"),
|
|
465
|
+
session: createSubsystemLogger("session"),
|
|
466
|
+
subagent: createSubsystemLogger("subagent"),
|
|
467
|
+
web: createSubsystemLogger("web"),
|
|
468
|
+
ws: createSubsystemLogger("ws"),
|
|
469
|
+
hook: createSubsystemLogger("hook"),
|
|
470
|
+
task: createSubsystemLogger("task"),
|
|
471
|
+
memory: createSubsystemLogger("memory"),
|
|
472
|
+
cron: createSubsystemLogger("cron"),
|
|
473
|
+
notif: createSubsystemLogger("notif"),
|
|
474
|
+
usage: createSubsystemLogger("usage"),
|
|
475
|
+
heartbeat: createSubsystemLogger("heartbeat"),
|
|
476
|
+
git: createSubsystemLogger("git"),
|
|
477
|
+
browser: createSubsystemLogger("browser"),
|
|
478
|
+
audio: createSubsystemLogger("audio"),
|
|
479
|
+
stt: createSubsystemLogger("stt"),
|
|
480
|
+
obs: createSubsystemLogger("obs"),
|
|
481
|
+
calendar: createSubsystemLogger("calendar")
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// src/utils/file-lock.ts
|
|
487
|
+
import fs3 from "fs";
|
|
488
|
+
import fsp2 from "fs/promises";
|
|
489
|
+
import path3 from "path";
|
|
490
|
+
function lockDir(filePath) {
|
|
491
|
+
return filePath + ".lock";
|
|
492
|
+
}
|
|
493
|
+
function pidFile(lock) {
|
|
494
|
+
return path3.join(lock, "pid");
|
|
495
|
+
}
|
|
496
|
+
function isPidAlive(pid) {
|
|
497
|
+
try {
|
|
498
|
+
process.kill(pid, 0);
|
|
499
|
+
return true;
|
|
500
|
+
} catch {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function withFileLock(filePath, fn) {
|
|
505
|
+
const lock = lockDir(filePath);
|
|
506
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
507
|
+
await fsp2.mkdir(path3.dirname(lock), { recursive: true });
|
|
508
|
+
while (true) {
|
|
509
|
+
try {
|
|
510
|
+
await fsp2.mkdir(lock);
|
|
511
|
+
try {
|
|
512
|
+
await fsp2.writeFile(pidFile(lock), String(process.pid));
|
|
513
|
+
} catch {
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
} catch (err) {
|
|
517
|
+
if (err.code !== "EEXIST") throw err;
|
|
518
|
+
if (await isLockStaleAsync(lock)) {
|
|
519
|
+
try {
|
|
520
|
+
await fsp2.rm(lock, { recursive: true, force: true });
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (Date.now() > deadline) {
|
|
526
|
+
throw new Error(`File lock timeout after ${TIMEOUT_MS}ms: ${lock}`);
|
|
527
|
+
}
|
|
528
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
return await fn();
|
|
533
|
+
} finally {
|
|
534
|
+
try {
|
|
535
|
+
await fsp2.rm(lock, { recursive: true, force: true });
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async function isLockStaleAsync(lock) {
|
|
541
|
+
try {
|
|
542
|
+
const raw = await fsp2.readFile(pidFile(lock), "utf-8");
|
|
543
|
+
const pid = parseInt(raw.trim(), 10);
|
|
544
|
+
if (!isNaN(pid) && pid > 0) {
|
|
545
|
+
return !isPidAlive(pid);
|
|
546
|
+
}
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const stat = await fsp2.stat(lock);
|
|
551
|
+
return Date.now() - stat.mtimeMs > STALE_AGE_MS;
|
|
552
|
+
} catch {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
var STALE_AGE_MS, TIMEOUT_MS, POLL_MS;
|
|
557
|
+
var init_file_lock = __esm({
|
|
558
|
+
"src/utils/file-lock.ts"() {
|
|
559
|
+
"use strict";
|
|
560
|
+
STALE_AGE_MS = 3e4;
|
|
561
|
+
TIMEOUT_MS = 1e4;
|
|
562
|
+
POLL_MS = 15;
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// src/utils/fs.ts
|
|
567
|
+
import fs4 from "fs/promises";
|
|
568
|
+
import path4 from "path";
|
|
569
|
+
import crypto from "crypto";
|
|
570
|
+
async function writeJsonFile(filePath, data) {
|
|
571
|
+
const dir = path4.dirname(filePath);
|
|
572
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
573
|
+
const tmpFile = path4.join(
|
|
574
|
+
dir,
|
|
575
|
+
`.open-walnut-${crypto.randomBytes(8).toString("hex")}.tmp`
|
|
576
|
+
);
|
|
577
|
+
try {
|
|
578
|
+
await fs4.writeFile(tmpFile, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
579
|
+
await fs4.rename(tmpFile, filePath);
|
|
580
|
+
} catch (err) {
|
|
581
|
+
await fs4.rm(tmpFile, { force: true }).catch(() => {
|
|
582
|
+
});
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function readJsonFile(filePath, fallback) {
|
|
587
|
+
let content;
|
|
588
|
+
try {
|
|
589
|
+
content = await fs4.readFile(filePath, "utf-8");
|
|
590
|
+
} catch (err) {
|
|
591
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
592
|
+
return fallback;
|
|
593
|
+
}
|
|
594
|
+
log.web.warn("non-ENOENT error reading JSON file", { filePath, error: err instanceof Error ? err.message : String(err) });
|
|
595
|
+
return fallback;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
return JSON.parse(content);
|
|
599
|
+
} catch (parseErr) {
|
|
600
|
+
if (content.trim().length === 0) {
|
|
601
|
+
return fallback;
|
|
602
|
+
}
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Failed to parse ${filePath}: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function ensureDir(dirPath) {
|
|
609
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
610
|
+
}
|
|
611
|
+
var init_fs = __esm({
|
|
612
|
+
"src/utils/fs.ts"() {
|
|
613
|
+
"use strict";
|
|
614
|
+
init_logging();
|
|
615
|
+
init_file_lock();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// src/core/types.ts
|
|
620
|
+
var VALID_PRIORITIES, SESSION_MODELS, SESSION_MODEL_CLI_MAP, VALID_SESSION_MODEL_IDS, SESSION_EFFORT_IDS, VALID_SESSION_EFFORT_IDS, SESSION_MODES, SESSION_MODE_IDS, VALID_SESSION_MODE_IDS, SESSION_MODE_CLI_MAP, SESSION_MODE_LABELS;
|
|
621
|
+
var init_types = __esm({
|
|
622
|
+
"src/core/types.ts"() {
|
|
623
|
+
"use strict";
|
|
624
|
+
VALID_PRIORITIES = ["immediate", "important", "backlog", "none"];
|
|
625
|
+
SESSION_MODELS = [
|
|
626
|
+
{ id: "opus", label: "Opus", description: "Most capable", cliModel: "opus", family: "opus", is1m: false },
|
|
627
|
+
{ id: "opus-1m", label: "Opus 1M", description: "1M context window", cliModel: "opus[1m]", family: "opus", is1m: true },
|
|
628
|
+
{ id: "sonnet", label: "Sonnet", description: "Balanced", cliModel: "sonnet", family: "sonnet", is1m: false },
|
|
629
|
+
{ id: "sonnet-1m", label: "Sonnet 1M", description: "1M context window", cliModel: "sonnet[1m]", family: "sonnet", is1m: true },
|
|
630
|
+
{ id: "haiku", label: "Haiku", description: "Fastest", cliModel: "haiku", family: "haiku", is1m: false },
|
|
631
|
+
{ id: "fable", label: "Fable", description: "Fast & capable", cliModel: "fable", family: "fable", is1m: false },
|
|
632
|
+
{ id: "fable-1m", label: "Fable 1M", description: "1M context window", cliModel: "fable[1m]", family: "fable", is1m: true }
|
|
633
|
+
];
|
|
634
|
+
SESSION_MODEL_CLI_MAP = Object.fromEntries(SESSION_MODELS.map((m) => [m.id, m.cliModel]));
|
|
635
|
+
VALID_SESSION_MODEL_IDS = new Set(SESSION_MODELS.map((m) => m.id));
|
|
636
|
+
SESSION_EFFORT_IDS = ["low", "medium", "high", "xhigh", "max"];
|
|
637
|
+
VALID_SESSION_EFFORT_IDS = new Set(SESSION_EFFORT_IDS);
|
|
638
|
+
SESSION_MODES = [
|
|
639
|
+
{ id: "plan", cli: "plan", label: "Plan", description: "Explore and plan only \u2014 no edits or commands" },
|
|
640
|
+
{ id: "default", cli: "default", label: "Default", description: "Ask before anything sensitive" },
|
|
641
|
+
{ id: "dontAsk", cli: "dontAsk", label: "Don't Ask", description: "Never prompt \u2014 deny whatever is not pre-approved" },
|
|
642
|
+
{ id: "accept", cli: "acceptEdits", label: "Accept", description: "Auto-accept file edits, still ask for the rest" },
|
|
643
|
+
{ id: "auto", cli: "auto", label: "Auto", description: "Classify each call and auto-allow the safe ones" },
|
|
644
|
+
{ id: "bypass", cli: "bypassPermissions", label: "Bypass", description: "Full trust \u2014 never ask, allow everything" }
|
|
645
|
+
];
|
|
646
|
+
SESSION_MODE_IDS = SESSION_MODES.map((m) => m.id);
|
|
647
|
+
VALID_SESSION_MODE_IDS = new Set(SESSION_MODE_IDS);
|
|
648
|
+
SESSION_MODE_CLI_MAP = Object.fromEntries(SESSION_MODES.map((m) => [m.id, m.cli]));
|
|
649
|
+
SESSION_MODE_LABELS = Object.fromEntries(SESSION_MODES.map((m) => [m.id, m.label]));
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// src/agent/providers/model-catalog.ts
|
|
654
|
+
var MODEL_CATALOG;
|
|
655
|
+
var init_model_catalog = __esm({
|
|
656
|
+
"src/agent/providers/model-catalog.ts"() {
|
|
657
|
+
"use strict";
|
|
658
|
+
MODEL_CATALOG = {
|
|
659
|
+
// ── AWS Bedrock (Claude via cross-region inference) ──
|
|
660
|
+
// Models with 1M variants use model_id to map back to the same API model ID.
|
|
661
|
+
// The API has no -1m model; 1M capability is activated via the context-1m beta header.
|
|
662
|
+
// model_id therefore redirects the -1m catalog entry to the real API endpoint.
|
|
663
|
+
bedrock: [
|
|
664
|
+
// Opus 5: 1M context is native and the only size — no -1m variant, no beta header.
|
|
665
|
+
{
|
|
666
|
+
id: "global.anthropic.claude-opus-5",
|
|
667
|
+
provider: "bedrock",
|
|
668
|
+
label: "Opus 5",
|
|
669
|
+
max_tokens: 128e3,
|
|
670
|
+
context_window: 1e6,
|
|
671
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true, native_1m: true }
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
id: "global.anthropic.claude-opus-4-8",
|
|
675
|
+
provider: "bedrock",
|
|
676
|
+
label: "Opus 4.8",
|
|
677
|
+
max_tokens: 128e3,
|
|
678
|
+
context_window: 2e5,
|
|
679
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
id: "global.anthropic.claude-opus-4-8-1m",
|
|
683
|
+
provider: "bedrock",
|
|
684
|
+
model_id: "global.anthropic.claude-opus-4-8",
|
|
685
|
+
label: "Opus 4.8 (1M)",
|
|
686
|
+
max_tokens: 128e3,
|
|
687
|
+
context_window: 1e6,
|
|
688
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: "global.anthropic.claude-opus-4-7",
|
|
692
|
+
provider: "bedrock",
|
|
693
|
+
label: "Opus 4.7",
|
|
694
|
+
max_tokens: 128e3,
|
|
695
|
+
context_window: 2e5,
|
|
696
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: "global.anthropic.claude-opus-4-7-1m",
|
|
700
|
+
provider: "bedrock",
|
|
701
|
+
model_id: "global.anthropic.claude-opus-4-7",
|
|
702
|
+
label: "Opus 4.7 (1M)",
|
|
703
|
+
max_tokens: 128e3,
|
|
704
|
+
context_window: 1e6,
|
|
705
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
id: "global.anthropic.claude-opus-4-6-v1",
|
|
709
|
+
provider: "bedrock",
|
|
710
|
+
label: "Opus 4.6",
|
|
711
|
+
max_tokens: 128e3,
|
|
712
|
+
context_window: 2e5,
|
|
713
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: "global.anthropic.claude-opus-4-6-v1-1m",
|
|
717
|
+
provider: "bedrock",
|
|
718
|
+
model_id: "global.anthropic.claude-opus-4-6-v1",
|
|
719
|
+
label: "Opus 4.6 (1M)",
|
|
720
|
+
max_tokens: 128e3,
|
|
721
|
+
context_window: 1e6,
|
|
722
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
id: "global.anthropic.claude-sonnet-4-6",
|
|
726
|
+
provider: "bedrock",
|
|
727
|
+
label: "Sonnet 4.6",
|
|
728
|
+
max_tokens: 64e3,
|
|
729
|
+
context_window: 2e5,
|
|
730
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
id: "global.anthropic.claude-sonnet-4-6-1m",
|
|
734
|
+
provider: "bedrock",
|
|
735
|
+
model_id: "global.anthropic.claude-sonnet-4-6",
|
|
736
|
+
label: "Sonnet 4.6 (1M)",
|
|
737
|
+
max_tokens: 64e3,
|
|
738
|
+
context_window: 1e6,
|
|
739
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
743
|
+
provider: "bedrock",
|
|
744
|
+
label: "Haiku 4.5",
|
|
745
|
+
max_tokens: 64e3,
|
|
746
|
+
context_window: 2e5
|
|
747
|
+
}
|
|
748
|
+
// No extended thinking — Haiku 4.5 doesn't support it
|
|
749
|
+
],
|
|
750
|
+
// ── Anthropic Direct API ──
|
|
751
|
+
anthropic: [
|
|
752
|
+
// Opus 5: 1M context is native and the only size — no -1m variant, no beta header.
|
|
753
|
+
{
|
|
754
|
+
id: "claude-opus-5",
|
|
755
|
+
provider: "anthropic",
|
|
756
|
+
label: "Opus 5",
|
|
757
|
+
max_tokens: 128e3,
|
|
758
|
+
context_window: 1e6,
|
|
759
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true, native_1m: true }
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
id: "claude-opus-4-8",
|
|
763
|
+
provider: "anthropic",
|
|
764
|
+
label: "Opus 4.8",
|
|
765
|
+
max_tokens: 128e3,
|
|
766
|
+
context_window: 2e5,
|
|
767
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
id: "claude-opus-4-8-1m",
|
|
771
|
+
provider: "anthropic",
|
|
772
|
+
model_id: "claude-opus-4-8",
|
|
773
|
+
label: "Opus 4.8 (1M)",
|
|
774
|
+
max_tokens: 128e3,
|
|
775
|
+
context_window: 1e6,
|
|
776
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
id: "claude-opus-4-7",
|
|
780
|
+
provider: "anthropic",
|
|
781
|
+
label: "Opus 4.7",
|
|
782
|
+
max_tokens: 128e3,
|
|
783
|
+
context_window: 2e5,
|
|
784
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
id: "claude-opus-4-7-1m",
|
|
788
|
+
provider: "anthropic",
|
|
789
|
+
model_id: "claude-opus-4-7",
|
|
790
|
+
label: "Opus 4.7 (1M)",
|
|
791
|
+
max_tokens: 128e3,
|
|
792
|
+
context_window: 1e6,
|
|
793
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
id: "claude-opus-4-6",
|
|
797
|
+
provider: "anthropic",
|
|
798
|
+
label: "Opus 4.6",
|
|
799
|
+
max_tokens: 128e3,
|
|
800
|
+
context_window: 2e5,
|
|
801
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
id: "claude-opus-4-6-1m",
|
|
805
|
+
provider: "anthropic",
|
|
806
|
+
model_id: "claude-opus-4-6",
|
|
807
|
+
label: "Opus 4.6 (1M)",
|
|
808
|
+
max_tokens: 128e3,
|
|
809
|
+
context_window: 1e6,
|
|
810
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
id: "claude-sonnet-4-6",
|
|
814
|
+
provider: "anthropic",
|
|
815
|
+
label: "Sonnet 4.6",
|
|
816
|
+
max_tokens: 64e3,
|
|
817
|
+
context_window: 2e5,
|
|
818
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
id: "claude-sonnet-4-6-1m",
|
|
822
|
+
provider: "anthropic",
|
|
823
|
+
model_id: "claude-sonnet-4-6",
|
|
824
|
+
label: "Sonnet 4.6 (1M)",
|
|
825
|
+
max_tokens: 64e3,
|
|
826
|
+
context_window: 1e6,
|
|
827
|
+
compat: { thinking_format: "anthropic", supports_adaptive: true }
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
id: "claude-haiku-4-5",
|
|
831
|
+
provider: "anthropic",
|
|
832
|
+
label: "Haiku 4.5",
|
|
833
|
+
max_tokens: 64e3,
|
|
834
|
+
context_window: 2e5
|
|
835
|
+
}
|
|
836
|
+
// No extended thinking — Haiku 4.5 doesn't support it
|
|
837
|
+
],
|
|
838
|
+
// ── OpenAI ──
|
|
839
|
+
openai: [
|
|
840
|
+
{
|
|
841
|
+
id: "gpt-5.4",
|
|
842
|
+
provider: "openai",
|
|
843
|
+
label: "GPT-5.4",
|
|
844
|
+
max_tokens: 128e3,
|
|
845
|
+
context_window: 105e4
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
id: "gpt-5-mini-2025-08-07",
|
|
849
|
+
provider: "openai",
|
|
850
|
+
label: "GPT-5 Mini",
|
|
851
|
+
max_tokens: 128e3,
|
|
852
|
+
context_window: 4e5
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
id: "gpt-5-nano-2025-08-07",
|
|
856
|
+
provider: "openai",
|
|
857
|
+
label: "GPT-5 Nano",
|
|
858
|
+
max_tokens: 128e3,
|
|
859
|
+
context_window: 4e5
|
|
860
|
+
}
|
|
861
|
+
],
|
|
862
|
+
// ── Google Gemini ──
|
|
863
|
+
gemini: [
|
|
864
|
+
{
|
|
865
|
+
id: "gemini-3.1-pro-preview",
|
|
866
|
+
provider: "gemini",
|
|
867
|
+
label: "Gemini 3.1 Pro",
|
|
868
|
+
max_tokens: 65536,
|
|
869
|
+
context_window: 1e6
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
id: "gemini-3-flash-preview",
|
|
873
|
+
provider: "gemini",
|
|
874
|
+
label: "Gemini 3 Flash",
|
|
875
|
+
max_tokens: 65536,
|
|
876
|
+
context_window: 1e6
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
id: "gemini-3.1-flash-lite-preview",
|
|
880
|
+
provider: "gemini",
|
|
881
|
+
label: "Gemini 3.1 Flash Lite",
|
|
882
|
+
max_tokens: 65536,
|
|
883
|
+
context_window: 1e6
|
|
884
|
+
}
|
|
885
|
+
],
|
|
886
|
+
// ── OpenRouter (aggregator — sorted by release date, newest first) ──
|
|
887
|
+
openrouter: [
|
|
888
|
+
// -- Anthropic --
|
|
889
|
+
{
|
|
890
|
+
id: "anthropic/claude-opus-5",
|
|
891
|
+
provider: "openrouter",
|
|
892
|
+
label: "Claude Opus 5",
|
|
893
|
+
max_tokens: 128e3,
|
|
894
|
+
context_window: 1e6
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: "anthropic/claude-opus-5-fast",
|
|
898
|
+
provider: "openrouter",
|
|
899
|
+
label: "Claude Opus 5 (Fast)",
|
|
900
|
+
max_tokens: 128e3,
|
|
901
|
+
context_window: 1e6
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
id: "anthropic/claude-opus-4.6",
|
|
905
|
+
provider: "openrouter",
|
|
906
|
+
label: "Claude Opus 4.6",
|
|
907
|
+
max_tokens: 128e3,
|
|
908
|
+
context_window: 1e6
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
id: "anthropic/claude-sonnet-4.6",
|
|
912
|
+
provider: "openrouter",
|
|
913
|
+
label: "Claude Sonnet 4.6",
|
|
914
|
+
max_tokens: 64e3,
|
|
915
|
+
context_window: 1e6
|
|
916
|
+
},
|
|
917
|
+
// -- OpenAI --
|
|
918
|
+
{
|
|
919
|
+
id: "openai/gpt-5.2",
|
|
920
|
+
provider: "openrouter",
|
|
921
|
+
label: "GPT-5.2",
|
|
922
|
+
max_tokens: 128e3,
|
|
923
|
+
context_window: 4e5
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
id: "openai/gpt-5.2-pro",
|
|
927
|
+
provider: "openrouter",
|
|
928
|
+
label: "GPT-5.2 Pro",
|
|
929
|
+
max_tokens: 128e3,
|
|
930
|
+
context_window: 4e5
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
id: "openai/gpt-5.2-chat",
|
|
934
|
+
provider: "openrouter",
|
|
935
|
+
label: "GPT-5.2 Chat",
|
|
936
|
+
max_tokens: 128e3,
|
|
937
|
+
context_window: 128e3
|
|
938
|
+
},
|
|
939
|
+
// -- Google --
|
|
940
|
+
{
|
|
941
|
+
id: "google/gemini-3.1-pro-preview",
|
|
942
|
+
provider: "openrouter",
|
|
943
|
+
label: "Gemini 3.1 Pro",
|
|
944
|
+
max_tokens: 65536,
|
|
945
|
+
context_window: 1048576
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
id: "google/gemini-3-flash-preview",
|
|
949
|
+
provider: "openrouter",
|
|
950
|
+
label: "Gemini 3 Flash",
|
|
951
|
+
max_tokens: 65536,
|
|
952
|
+
context_window: 1048576
|
|
953
|
+
},
|
|
954
|
+
// -- Qwen --
|
|
955
|
+
{
|
|
956
|
+
id: "qwen/qwen3.5-plus-02-15",
|
|
957
|
+
provider: "openrouter",
|
|
958
|
+
label: "Qwen 3.5 Plus",
|
|
959
|
+
max_tokens: 128e3,
|
|
960
|
+
context_window: 1e6
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
id: "qwen/qwen3.5-397b-a17b",
|
|
964
|
+
provider: "openrouter",
|
|
965
|
+
label: "Qwen 3.5 397B",
|
|
966
|
+
max_tokens: 128e3,
|
|
967
|
+
context_window: 262144
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
id: "qwen/qwen3-max-thinking",
|
|
971
|
+
provider: "openrouter",
|
|
972
|
+
label: "Qwen3 Max Thinking",
|
|
973
|
+
max_tokens: 128e3,
|
|
974
|
+
context_window: 262144
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
id: "qwen/qwen3-coder-next",
|
|
978
|
+
provider: "openrouter",
|
|
979
|
+
label: "Qwen3 Coder Next",
|
|
980
|
+
max_tokens: 128e3,
|
|
981
|
+
context_window: 262144
|
|
982
|
+
},
|
|
983
|
+
// -- MiniMax --
|
|
984
|
+
{
|
|
985
|
+
id: "minimax/minimax-m2.5",
|
|
986
|
+
provider: "openrouter",
|
|
987
|
+
label: "MiniMax M2.5",
|
|
988
|
+
max_tokens: 128e3,
|
|
989
|
+
context_window: 196608
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
id: "minimax/minimax-m2.1",
|
|
993
|
+
provider: "openrouter",
|
|
994
|
+
label: "MiniMax M2.1",
|
|
995
|
+
max_tokens: 128e3,
|
|
996
|
+
context_window: 196608
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: "minimax/minimax-m2-her",
|
|
1000
|
+
provider: "openrouter",
|
|
1001
|
+
label: "MiniMax M2 Her",
|
|
1002
|
+
max_tokens: 128e3,
|
|
1003
|
+
context_window: 65536
|
|
1004
|
+
},
|
|
1005
|
+
// -- Z.ai (GLM) --
|
|
1006
|
+
{
|
|
1007
|
+
id: "z-ai/glm-5",
|
|
1008
|
+
provider: "openrouter",
|
|
1009
|
+
label: "GLM 5",
|
|
1010
|
+
max_tokens: 128e3,
|
|
1011
|
+
context_window: 202752
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
id: "z-ai/glm-4.7",
|
|
1015
|
+
provider: "openrouter",
|
|
1016
|
+
label: "GLM 4.7",
|
|
1017
|
+
max_tokens: 128e3,
|
|
1018
|
+
context_window: 202752
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
id: "z-ai/glm-4.7-flash",
|
|
1022
|
+
provider: "openrouter",
|
|
1023
|
+
label: "GLM 4.7 Flash",
|
|
1024
|
+
max_tokens: 128e3,
|
|
1025
|
+
context_window: 202752
|
|
1026
|
+
},
|
|
1027
|
+
// -- Moonshot / Kimi --
|
|
1028
|
+
{
|
|
1029
|
+
id: "moonshotai/kimi-k2.5",
|
|
1030
|
+
provider: "openrouter",
|
|
1031
|
+
label: "Kimi K2.5",
|
|
1032
|
+
max_tokens: 128e3,
|
|
1033
|
+
context_window: 262144
|
|
1034
|
+
},
|
|
1035
|
+
// -- AionLabs --
|
|
1036
|
+
{
|
|
1037
|
+
id: "aion-labs/aion-2.0",
|
|
1038
|
+
provider: "openrouter",
|
|
1039
|
+
label: "Aion 2.0",
|
|
1040
|
+
max_tokens: 128e3,
|
|
1041
|
+
context_window: 131072
|
|
1042
|
+
},
|
|
1043
|
+
// -- StepFun --
|
|
1044
|
+
{
|
|
1045
|
+
id: "stepfun/step-3.5-flash",
|
|
1046
|
+
provider: "openrouter",
|
|
1047
|
+
label: "Step 3.5 Flash",
|
|
1048
|
+
max_tokens: 128e3,
|
|
1049
|
+
context_window: 256e3
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
id: "stepfun/step-3.5-flash:free",
|
|
1053
|
+
provider: "openrouter",
|
|
1054
|
+
label: "Step 3.5 Flash (free)",
|
|
1055
|
+
max_tokens: 128e3,
|
|
1056
|
+
context_window: 256e3
|
|
1057
|
+
},
|
|
1058
|
+
// -- ByteDance Seed --
|
|
1059
|
+
{
|
|
1060
|
+
id: "bytedance-seed/seed-1.6",
|
|
1061
|
+
provider: "openrouter",
|
|
1062
|
+
label: "Seed 1.6",
|
|
1063
|
+
max_tokens: 128e3,
|
|
1064
|
+
context_window: 262144
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
id: "bytedance-seed/seed-1.6-flash",
|
|
1068
|
+
provider: "openrouter",
|
|
1069
|
+
label: "Seed 1.6 Flash",
|
|
1070
|
+
max_tokens: 128e3,
|
|
1071
|
+
context_window: 262144
|
|
1072
|
+
},
|
|
1073
|
+
// -- Xiaomi --
|
|
1074
|
+
{
|
|
1075
|
+
id: "xiaomi/mimo-v2-flash",
|
|
1076
|
+
provider: "openrouter",
|
|
1077
|
+
label: "MiMo V2 Flash",
|
|
1078
|
+
max_tokens: 128e3,
|
|
1079
|
+
context_window: 262144
|
|
1080
|
+
},
|
|
1081
|
+
// -- NVIDIA --
|
|
1082
|
+
{
|
|
1083
|
+
id: "nvidia/nemotron-3-nano-30b-a3b",
|
|
1084
|
+
provider: "openrouter",
|
|
1085
|
+
label: "Nemotron 3 Nano 30B",
|
|
1086
|
+
max_tokens: 128e3,
|
|
1087
|
+
context_window: 262144
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
id: "nvidia/nemotron-3-nano-30b-a3b:free",
|
|
1091
|
+
provider: "openrouter",
|
|
1092
|
+
label: "Nemotron 3 Nano 30B (free)",
|
|
1093
|
+
max_tokens: 128e3,
|
|
1094
|
+
context_window: 256e3
|
|
1095
|
+
},
|
|
1096
|
+
// -- Writer --
|
|
1097
|
+
{
|
|
1098
|
+
id: "writer/palmyra-x5",
|
|
1099
|
+
provider: "openrouter",
|
|
1100
|
+
label: "Palmyra X5",
|
|
1101
|
+
max_tokens: 128e3,
|
|
1102
|
+
context_window: 104e4
|
|
1103
|
+
},
|
|
1104
|
+
// -- Upstage --
|
|
1105
|
+
{
|
|
1106
|
+
id: "upstage/solar-pro-3",
|
|
1107
|
+
provider: "openrouter",
|
|
1108
|
+
label: "Solar Pro 3",
|
|
1109
|
+
max_tokens: 128e3,
|
|
1110
|
+
context_window: 128e3
|
|
1111
|
+
},
|
|
1112
|
+
// -- Mistral --
|
|
1113
|
+
{
|
|
1114
|
+
id: "mistralai/mistral-small-creative",
|
|
1115
|
+
provider: "openrouter",
|
|
1116
|
+
label: "Mistral Small Creative",
|
|
1117
|
+
max_tokens: 128e3,
|
|
1118
|
+
context_window: 32768
|
|
1119
|
+
},
|
|
1120
|
+
// -- AllenAI --
|
|
1121
|
+
{
|
|
1122
|
+
id: "allenai/olmo-3.1-32b-instruct",
|
|
1123
|
+
provider: "openrouter",
|
|
1124
|
+
label: "OLMo 3.1 32B Instruct",
|
|
1125
|
+
max_tokens: 128e3,
|
|
1126
|
+
context_window: 65536
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
id: "allenai/olmo-3.1-32b-think",
|
|
1130
|
+
provider: "openrouter",
|
|
1131
|
+
label: "OLMo 3.1 32B Think",
|
|
1132
|
+
max_tokens: 128e3,
|
|
1133
|
+
context_window: 65536
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: "allenai/molmo-2-8b",
|
|
1137
|
+
provider: "openrouter",
|
|
1138
|
+
label: "Molmo2 8B",
|
|
1139
|
+
max_tokens: 128e3,
|
|
1140
|
+
context_window: 36864
|
|
1141
|
+
},
|
|
1142
|
+
// -- Arcee --
|
|
1143
|
+
{
|
|
1144
|
+
id: "arcee-ai/trinity-large-preview:free",
|
|
1145
|
+
provider: "openrouter",
|
|
1146
|
+
label: "Trinity Large Preview (free)",
|
|
1147
|
+
max_tokens: 128e3,
|
|
1148
|
+
context_window: 131e3
|
|
1149
|
+
}
|
|
1150
|
+
],
|
|
1151
|
+
// ── Ollama (local) — empty baseline, user adds their installed models ──
|
|
1152
|
+
ollama: [],
|
|
1153
|
+
// ── Claude Code subscription (local `claude -p`, text-only) ──
|
|
1154
|
+
// The CLI picks the actual model from the subscription; these are short
|
|
1155
|
+
// aliases the adapter forwards as --model (a bare alias resolves server-side).
|
|
1156
|
+
// 'default' means "let the subscription decide" (adapter sends no --model).
|
|
1157
|
+
claude_cli: [
|
|
1158
|
+
{ id: "default", provider: "claude_cli", label: "Subscription default", context_window: 2e5 },
|
|
1159
|
+
{ id: "opus", provider: "claude_cli", label: "Opus (subscription)", context_window: 2e5 },
|
|
1160
|
+
{ id: "sonnet", provider: "claude_cli", label: "Sonnet (subscription)", context_window: 2e5 },
|
|
1161
|
+
{ id: "haiku", provider: "claude_cli", label: "Haiku (subscription)", context_window: 2e5 }
|
|
1162
|
+
]
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
// src/core/ssh-config-scanner.ts
|
|
1168
|
+
import fs5 from "fs/promises";
|
|
1169
|
+
import path5 from "path";
|
|
1170
|
+
import os2 from "os";
|
|
1171
|
+
function isGitServerHost(h) {
|
|
1172
|
+
if (h.user === "git") return true;
|
|
1173
|
+
const gitPrefix = /^(git|github|gitlab|bitbucket|codecommit|ssh\.github)\./i;
|
|
1174
|
+
return gitPrefix.test(h.alias) || gitPrefix.test(h.hostname);
|
|
1175
|
+
}
|
|
1176
|
+
function shortLabelForFqdn(hostname) {
|
|
1177
|
+
const first = hostname.split(".")[0];
|
|
1178
|
+
if (first.length <= 24) return first;
|
|
1179
|
+
const parts = first.split("-");
|
|
1180
|
+
if (parts.length >= 3) {
|
|
1181
|
+
return `${parts[0]}-${parts[1]}\u2026${parts[parts.length - 1]}`;
|
|
1182
|
+
}
|
|
1183
|
+
return `${first.slice(0, 12)}\u2026${first.slice(-8)}`;
|
|
1184
|
+
}
|
|
1185
|
+
async function scanSshConfig(configPath) {
|
|
1186
|
+
const sshConfigPath = configPath ?? path5.join(os2.homedir(), ".ssh", "config");
|
|
1187
|
+
let mtimeMs2;
|
|
1188
|
+
try {
|
|
1189
|
+
mtimeMs2 = (await fs5.stat(sshConfigPath)).mtimeMs;
|
|
1190
|
+
} catch {
|
|
1191
|
+
return /* @__PURE__ */ new Map();
|
|
1192
|
+
}
|
|
1193
|
+
const cached = scanCache.get(sshConfigPath);
|
|
1194
|
+
if (cached && cached.mtimeMs === mtimeMs2) return cached.result;
|
|
1195
|
+
const result = await parseSshConfigFile(sshConfigPath);
|
|
1196
|
+
scanCache.set(sshConfigPath, { mtimeMs: mtimeMs2, result });
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
async function parseSshConfigFile(sshConfigPath) {
|
|
1200
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
1201
|
+
try {
|
|
1202
|
+
const content = await fs5.readFile(sshConfigPath, "utf-8");
|
|
1203
|
+
const lines = content.split("\n");
|
|
1204
|
+
let currentHost = null;
|
|
1205
|
+
for (const rawLine of lines) {
|
|
1206
|
+
const line = rawLine.trim();
|
|
1207
|
+
if (!line || line.startsWith("#")) continue;
|
|
1208
|
+
if (line.match(/^Host\s+/i)) {
|
|
1209
|
+
if (currentHost && currentHost.hostname) {
|
|
1210
|
+
discovered.set(currentHost.alias, currentHost);
|
|
1211
|
+
}
|
|
1212
|
+
const hostMatch = line.match(/^Host\s+(\S+)/i);
|
|
1213
|
+
if (hostMatch) {
|
|
1214
|
+
const alias = hostMatch[1];
|
|
1215
|
+
if (alias.includes("*") || alias.includes("?")) {
|
|
1216
|
+
currentHost = null;
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
currentHost = { alias, hostname: alias };
|
|
1220
|
+
}
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
if (!currentHost) continue;
|
|
1224
|
+
const hostnameMatch = line.match(/^HostName\s+(\S+)/i);
|
|
1225
|
+
if (hostnameMatch) {
|
|
1226
|
+
currentHost.hostname = hostnameMatch[1];
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const userMatch = line.match(/^User\s+(\S+)/i);
|
|
1230
|
+
if (userMatch) {
|
|
1231
|
+
currentHost.user = userMatch[1];
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
const portMatch = line.match(/^Port\s+(\d+)/i);
|
|
1235
|
+
if (portMatch) {
|
|
1236
|
+
currentHost.port = Number.parseInt(portMatch[1], 10);
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (currentHost && currentHost.hostname) {
|
|
1241
|
+
discovered.set(currentHost.alias, currentHost);
|
|
1242
|
+
}
|
|
1243
|
+
for (const [alias, h] of discovered) {
|
|
1244
|
+
if (isGitServerHost(h)) discovered.delete(alias);
|
|
1245
|
+
}
|
|
1246
|
+
const byHostname = /* @__PURE__ */ new Map();
|
|
1247
|
+
for (const h of discovered.values()) {
|
|
1248
|
+
const key = h.hostname.toLowerCase();
|
|
1249
|
+
const existing = byHostname.get(key);
|
|
1250
|
+
if (!existing) {
|
|
1251
|
+
byHostname.set(key, h);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
const existingIsShortAlias = existing.alias !== existing.hostname;
|
|
1255
|
+
const candidateIsShortAlias = h.alias !== h.hostname;
|
|
1256
|
+
if (candidateIsShortAlias && !existingIsShortAlias) byHostname.set(key, h);
|
|
1257
|
+
}
|
|
1258
|
+
const result = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const h of byHostname.values()) {
|
|
1260
|
+
if (h.alias === h.hostname && !h.label) h.label = shortLabelForFqdn(h.hostname);
|
|
1261
|
+
result.set(h.alias, h);
|
|
1262
|
+
}
|
|
1263
|
+
log.session.info("ssh-config-scanner: discovered hosts", {
|
|
1264
|
+
count: result.size,
|
|
1265
|
+
dropped: discovered.size - result.size
|
|
1266
|
+
});
|
|
1267
|
+
return result;
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
if (err.code === "ENOENT") {
|
|
1270
|
+
log.session.debug("ssh-config-scanner: no ~/.ssh/config found");
|
|
1271
|
+
} else {
|
|
1272
|
+
log.session.warn("ssh-config-scanner: failed to parse SSH config", {
|
|
1273
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return discovered;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
var scanCache;
|
|
1280
|
+
var init_ssh_config_scanner = __esm({
|
|
1281
|
+
"src/core/ssh-config-scanner.ts"() {
|
|
1282
|
+
"use strict";
|
|
1283
|
+
init_logging();
|
|
1284
|
+
scanCache = /* @__PURE__ */ new Map();
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// src/core/config-manager.ts
|
|
1289
|
+
import fs6 from "fs/promises";
|
|
1290
|
+
import yaml from "js-yaml";
|
|
1291
|
+
function migrateConfigToProjectOnly(config) {
|
|
1292
|
+
let changed = false;
|
|
1293
|
+
const defaults = config.defaults;
|
|
1294
|
+
if (defaults && "category" in defaults) {
|
|
1295
|
+
delete defaults.category;
|
|
1296
|
+
changed = true;
|
|
1297
|
+
}
|
|
1298
|
+
if ("local" in config) {
|
|
1299
|
+
delete config.local;
|
|
1300
|
+
changed = true;
|
|
1301
|
+
}
|
|
1302
|
+
const favorites = config.favorites;
|
|
1303
|
+
if (favorites && "categories" in favorites) {
|
|
1304
|
+
delete favorites.categories;
|
|
1305
|
+
changed = true;
|
|
1306
|
+
}
|
|
1307
|
+
const ordering = config.ordering;
|
|
1308
|
+
if (ordering && ("categories" in ordering || !Array.isArray(ordering.projects))) {
|
|
1309
|
+
const catOrder = Array.isArray(ordering.categories) ? ordering.categories : [];
|
|
1310
|
+
const nested = ordering.projects;
|
|
1311
|
+
const flat = [];
|
|
1312
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1313
|
+
const push = (raw) => {
|
|
1314
|
+
const name = typeof raw === "string" ? raw.trim() : "";
|
|
1315
|
+
if (!name) return;
|
|
1316
|
+
const key = name.toLowerCase();
|
|
1317
|
+
if (seen.has(key)) return;
|
|
1318
|
+
seen.add(key);
|
|
1319
|
+
flat.push(name);
|
|
1320
|
+
};
|
|
1321
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
1322
|
+
const byCategory = nested;
|
|
1323
|
+
const ordered = [
|
|
1324
|
+
...catOrder.map((c) => typeof c === "string" ? c : ""),
|
|
1325
|
+
...Object.keys(byCategory).filter(
|
|
1326
|
+
(k) => !catOrder.some((c) => typeof c === "string" && c.toLowerCase() === k.toLowerCase())
|
|
1327
|
+
)
|
|
1328
|
+
];
|
|
1329
|
+
for (const cat of ordered) {
|
|
1330
|
+
const key = Object.keys(byCategory).find((k) => k.toLowerCase() === cat.toLowerCase());
|
|
1331
|
+
if (!key) continue;
|
|
1332
|
+
const list = byCategory[key];
|
|
1333
|
+
if (Array.isArray(list)) for (const p of list) push(p);
|
|
1334
|
+
}
|
|
1335
|
+
} else if (Array.isArray(nested)) {
|
|
1336
|
+
for (const p of nested) push(p);
|
|
1337
|
+
}
|
|
1338
|
+
delete ordering.categories;
|
|
1339
|
+
ordering.projects = flat;
|
|
1340
|
+
changed = true;
|
|
1341
|
+
}
|
|
1342
|
+
const plugins = config.plugins;
|
|
1343
|
+
for (const entry of Object.values(plugins ?? {})) {
|
|
1344
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1345
|
+
const plugin = entry;
|
|
1346
|
+
if ("category" in plugin) {
|
|
1347
|
+
if (plugin.project === void 0 && typeof plugin.category === "string") {
|
|
1348
|
+
plugin.project = plugin.category;
|
|
1349
|
+
}
|
|
1350
|
+
delete plugin.category;
|
|
1351
|
+
changed = true;
|
|
1352
|
+
}
|
|
1353
|
+
if ("categories" in plugin) {
|
|
1354
|
+
delete plugin.categories;
|
|
1355
|
+
changed = true;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return changed;
|
|
1359
|
+
}
|
|
1360
|
+
async function readRawConfigContent() {
|
|
1361
|
+
try {
|
|
1362
|
+
return await fs6.readFile(CONFIG_FILE, "utf-8");
|
|
1363
|
+
} catch (primaryErr) {
|
|
1364
|
+
let backup;
|
|
1365
|
+
try {
|
|
1366
|
+
backup = await fs6.readFile(CONFIG_BACKUP_FILE, "utf-8");
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
if (!backup.trim()) return null;
|
|
1371
|
+
log.session.error("config-manager: config.yaml missing/unreadable \u2014 recovering from config.yaml.bak", {
|
|
1372
|
+
error: primaryErr instanceof Error ? primaryErr.message : String(primaryErr),
|
|
1373
|
+
backup: CONFIG_BACKUP_FILE
|
|
1374
|
+
});
|
|
1375
|
+
try {
|
|
1376
|
+
await fs6.writeFile(CONFIG_FILE, backup, "utf-8");
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
return backup;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async function writeConfigWithBackup(content) {
|
|
1383
|
+
await fs6.writeFile(CONFIG_FILE, content, "utf-8");
|
|
1384
|
+
try {
|
|
1385
|
+
await fs6.writeFile(CONFIG_BACKUP_FILE, content, "utf-8");
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
log.session.warn("config-manager: failed to update config backup", {
|
|
1388
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
async function getConfig() {
|
|
1393
|
+
try {
|
|
1394
|
+
const content = await readRawConfigContent();
|
|
1395
|
+
if (content === null) throw new Error("no config file and no backup");
|
|
1396
|
+
const parsed = yaml.load(content);
|
|
1397
|
+
const config = { ...DEFAULT_CONFIG, ...parsed };
|
|
1398
|
+
migrateConfigToProjectOnly(config);
|
|
1399
|
+
if (config.defaults?.priority && !VALID_PRIORITIES.includes(config.defaults.priority)) {
|
|
1400
|
+
const p = config.defaults.priority;
|
|
1401
|
+
if (p === "high") config.defaults.priority = "immediate";
|
|
1402
|
+
else if (p === "medium" || p === "low") config.defaults.priority = "backlog";
|
|
1403
|
+
else config.defaults.priority = "none";
|
|
1404
|
+
}
|
|
1405
|
+
if (!config.agent?.available_models) {
|
|
1406
|
+
config.agent = { ...config.agent, available_models: (MODEL_CATALOG.bedrock ?? []).map((m) => m.id) };
|
|
1407
|
+
}
|
|
1408
|
+
if (!config.agent?.main_model) {
|
|
1409
|
+
config.agent = { ...config.agent, main_model: (MODEL_CATALOG.bedrock ?? [])[0]?.id };
|
|
1410
|
+
}
|
|
1411
|
+
await mergeSshDiscoveredHosts(config);
|
|
1412
|
+
return config;
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
const firstRun = err instanceof Error && err.message === "no config file and no backup";
|
|
1415
|
+
const detail = { error: err instanceof Error ? err.message : String(err), configFile: CONFIG_FILE };
|
|
1416
|
+
if (firstRun) log.session.info("config-manager: no config.yaml and no backup \u2014 first run, using defaults", detail);
|
|
1417
|
+
else log.session.error("config-manager: config.yaml UNREADABLE and backup did not cover it \u2014 falling back to DEFAULTS. Machine-local settings (hosts/plugins/stt/provider) are missing until this is fixed.", detail);
|
|
1418
|
+
const defaultModels = (MODEL_CATALOG.bedrock ?? []).map((m) => m.id);
|
|
1419
|
+
return { ...DEFAULT_CONFIG, agent: { available_models: defaultModels, main_model: defaultModels[0] } };
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
async function mergeSshDiscoveredHosts(config) {
|
|
1423
|
+
const discovered = await scanSshConfig();
|
|
1424
|
+
if (discovered.size === 0) return;
|
|
1425
|
+
if (!config.hosts) {
|
|
1426
|
+
config.hosts = {};
|
|
1427
|
+
}
|
|
1428
|
+
const knownHostnames = new Set(
|
|
1429
|
+
Object.values(config.hosts).map((h) => h.hostname.toLowerCase())
|
|
1430
|
+
);
|
|
1431
|
+
for (const [alias, host] of discovered) {
|
|
1432
|
+
if (config.hosts[alias] || knownHostnames.has(host.hostname.toLowerCase())) {
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
config.hosts[alias] = {
|
|
1436
|
+
hostname: host.hostname,
|
|
1437
|
+
user: host.user,
|
|
1438
|
+
port: host.port,
|
|
1439
|
+
label: host.label,
|
|
1440
|
+
enabled: true,
|
|
1441
|
+
discovered: true
|
|
1442
|
+
};
|
|
1443
|
+
knownHostnames.add(host.hostname.toLowerCase());
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function withWriteLock(fn) {
|
|
1447
|
+
const prev = writeLock;
|
|
1448
|
+
let resolve;
|
|
1449
|
+
writeLock = new Promise((r) => {
|
|
1450
|
+
resolve = r;
|
|
1451
|
+
});
|
|
1452
|
+
return prev.then(fn).finally(() => resolve());
|
|
1453
|
+
}
|
|
1454
|
+
async function updateConfig(partial) {
|
|
1455
|
+
return withWriteLock(async () => {
|
|
1456
|
+
let existing = {};
|
|
1457
|
+
try {
|
|
1458
|
+
const content2 = await readRawConfigContent();
|
|
1459
|
+
existing = content2 === null ? {} : yaml.load(content2) ?? {};
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
log.session.warn("config-manager: existing config unreadable, starting from empty", {
|
|
1462
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
const merged = { ...existing };
|
|
1466
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
1467
|
+
if (value !== void 0) {
|
|
1468
|
+
merged[key] = value;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
let content = yaml.dump(merged, { indent: 2, lineWidth: 120 });
|
|
1472
|
+
content = content.replace(
|
|
1473
|
+
/^(\s+)available_models:/m,
|
|
1474
|
+
"$1# Available models for the agent form dropdown.\n$1# Edit this list to add or remove models.\n$1available_models:"
|
|
1475
|
+
);
|
|
1476
|
+
await writeConfigWithBackup(content);
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async function migrateConfigFileToProjectOnly() {
|
|
1480
|
+
return withWriteLock(async () => {
|
|
1481
|
+
let parsed;
|
|
1482
|
+
try {
|
|
1483
|
+
const content2 = await readRawConfigContent();
|
|
1484
|
+
if (content2 === null) return false;
|
|
1485
|
+
parsed = yaml.load(content2) ?? {};
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
log.session.warn("config-manager: skipping project-only migration, config unreadable", {
|
|
1488
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1489
|
+
});
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
if (!migrateConfigToProjectOnly(parsed)) return false;
|
|
1493
|
+
let content = yaml.dump(parsed, { indent: 2, lineWidth: 120 });
|
|
1494
|
+
content = content.replace(
|
|
1495
|
+
/^(\s+)available_models:/m,
|
|
1496
|
+
"$1# Available models for the agent form dropdown.\n$1# Edit this list to add or remove models.\n$1available_models:"
|
|
1497
|
+
);
|
|
1498
|
+
await writeConfigWithBackup(content);
|
|
1499
|
+
log.session.info("config-manager: migrated config.yaml to the project-only model");
|
|
1500
|
+
return true;
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
async function seedConfigDefaults() {
|
|
1504
|
+
await migrateConfigFileToProjectOnly();
|
|
1505
|
+
const config = await getConfig();
|
|
1506
|
+
let needsWrite = false;
|
|
1507
|
+
try {
|
|
1508
|
+
const raw = await fs6.readFile(CONFIG_FILE, "utf-8");
|
|
1509
|
+
if (!raw.includes("available_models")) {
|
|
1510
|
+
needsWrite = true;
|
|
1511
|
+
}
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
log.session.debug("config-manager: no config file found, will write defaults", {
|
|
1514
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1515
|
+
});
|
|
1516
|
+
needsWrite = true;
|
|
1517
|
+
}
|
|
1518
|
+
if (needsWrite) {
|
|
1519
|
+
await updateConfig({
|
|
1520
|
+
agent: config.agent,
|
|
1521
|
+
version: config.version,
|
|
1522
|
+
user: config.user,
|
|
1523
|
+
defaults: config.defaults,
|
|
1524
|
+
provider: config.provider
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
var DEFAULT_CONFIG, CONFIG_BACKUP_FILE, writeLock;
|
|
1529
|
+
var init_config_manager = __esm({
|
|
1530
|
+
"src/core/config-manager.ts"() {
|
|
1531
|
+
"use strict";
|
|
1532
|
+
init_logging();
|
|
1533
|
+
init_constants();
|
|
1534
|
+
init_types();
|
|
1535
|
+
init_model_catalog();
|
|
1536
|
+
init_ssh_config_scanner();
|
|
1537
|
+
DEFAULT_CONFIG = {
|
|
1538
|
+
version: 1,
|
|
1539
|
+
user: {},
|
|
1540
|
+
// New tasks default to Inbox (no project) and stay local, so quick-add is
|
|
1541
|
+
// instant and never silently routed to an external sync target.
|
|
1542
|
+
// `platform: 'local'` makes the source explicit. Existing users keep whatever
|
|
1543
|
+
// `defaults` is already on disk (getConfig spreads parsed over DEFAULT_CONFIG),
|
|
1544
|
+
// so this never re-routes an established setup.
|
|
1545
|
+
defaults: { priority: "none", platform: "local" },
|
|
1546
|
+
provider: { type: "claude-code" }
|
|
1547
|
+
};
|
|
1548
|
+
CONFIG_BACKUP_FILE = `${CONFIG_FILE}.bak`;
|
|
1549
|
+
writeLock = Promise.resolve();
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// src/core/event-types.ts
|
|
1554
|
+
var init_event_types = __esm({
|
|
1555
|
+
"src/core/event-types.ts"() {
|
|
1556
|
+
"use strict";
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// src/core/event-bus.ts
|
|
1561
|
+
import { randomBytes } from "crypto";
|
|
1562
|
+
var EventNames, EVENT_HISTORY_SIZE, eventHistory, KEY_BUS_EVENTS, EventBus, bus;
|
|
1563
|
+
var init_event_bus = __esm({
|
|
1564
|
+
"src/core/event-bus.ts"() {
|
|
1565
|
+
"use strict";
|
|
1566
|
+
init_logging();
|
|
1567
|
+
init_event_types();
|
|
1568
|
+
EventNames = {
|
|
1569
|
+
// Task events
|
|
1570
|
+
TASK_CREATED: "task:created",
|
|
1571
|
+
TASK_UPDATED: "task:updated",
|
|
1572
|
+
TASK_COMPLETED: "task:completed",
|
|
1573
|
+
TASK_STARRED: "task:starred",
|
|
1574
|
+
TASK_DELETED: "task:deleted",
|
|
1575
|
+
TASK_REORDERED: "task:reordered",
|
|
1576
|
+
TASK_UNBLOCKED: "task:unblocked",
|
|
1577
|
+
TASK_GROUPS_CHANGED: "task:groups-changed",
|
|
1578
|
+
// Calendar (external calendar events cache refreshed / event written)
|
|
1579
|
+
CALENDAR_UPDATED: "calendar:updated",
|
|
1580
|
+
// Inline subagent streaming
|
|
1581
|
+
AGENT_SUBAGENT_STREAM: "agent:subagent-stream",
|
|
1582
|
+
// Agent events (chat streaming)
|
|
1583
|
+
AGENT_TEXT_DELTA: "agent:text-delta",
|
|
1584
|
+
AGENT_TOOL_ACTIVITY: "agent:tool-activity",
|
|
1585
|
+
AGENT_TOOL_CALL: "agent:tool-call",
|
|
1586
|
+
AGENT_TOOL_RESULT: "agent:tool-result",
|
|
1587
|
+
AGENT_THINKING: "agent:thinking",
|
|
1588
|
+
AGENT_RESPONSE: "agent:response",
|
|
1589
|
+
AGENT_ERROR: "agent:error",
|
|
1590
|
+
// Session events
|
|
1591
|
+
SESSION_START: "session:start",
|
|
1592
|
+
SESSION_SEND: "session:send",
|
|
1593
|
+
SESSION_INTERRUPT: "session:interrupt",
|
|
1594
|
+
SESSION_STARTED: "session:started",
|
|
1595
|
+
SESSION_ENDED: "session:ended",
|
|
1596
|
+
SESSION_DELETED: "session:deleted",
|
|
1597
|
+
SESSION_CONTENT_UPDATED: "session:content-updated",
|
|
1598
|
+
SESSION_RESULT: "session:result",
|
|
1599
|
+
SESSION_ERROR: "session:error",
|
|
1600
|
+
// Session streaming events (from --output-format stream-json)
|
|
1601
|
+
SESSION_TEXT_DELTA: "session:text-delta",
|
|
1602
|
+
SESSION_THINKING_DELTA: "session:thinking-delta",
|
|
1603
|
+
SESSION_TOOL_USE: "session:tool-use",
|
|
1604
|
+
SESSION_TOOL_RESULT: "session:tool-result",
|
|
1605
|
+
SESSION_UNKNOWN_EVENT: "session:unknown-event",
|
|
1606
|
+
SESSION_STATUS_CHANGED: "session:status-changed",
|
|
1607
|
+
SESSION_MESSAGES_DELIVERED: "session:messages-delivered",
|
|
1608
|
+
SESSION_BATCH_COMPLETED: "session:batch-completed",
|
|
1609
|
+
SESSION_BATCH_FAILED: "session:batch-failed",
|
|
1610
|
+
SESSION_MESSAGE_QUEUED: "session:message-queued",
|
|
1611
|
+
SESSION_SYSTEM_EVENT: "session:system-event",
|
|
1612
|
+
SESSION_BACKGROUND_TASKS: "session:background-tasks",
|
|
1613
|
+
SESSION_USAGE_UPDATE: "session:usage-update",
|
|
1614
|
+
SESSION_SETTINGS_APPLIED: "session:settings-applied",
|
|
1615
|
+
SESSION_MODEL_CATALOG: "session:model-catalog",
|
|
1616
|
+
SESSION_PERMISSION_REQUEST: "session:permission-request",
|
|
1617
|
+
SESSION_PERMISSION_RESOLVED: "session:permission-resolved",
|
|
1618
|
+
// Side question ("/btw") — native Claude Code side_question round-trip results.
|
|
1619
|
+
// The drawer subscribes to these to update without polling.
|
|
1620
|
+
SESSION_SIDE_QUESTION_DONE: "session:side-question-done",
|
|
1621
|
+
SESSION_SIDE_QUESTION_ERROR: "session:side-question-error",
|
|
1622
|
+
// Team events (Claude Code Teams — parallel agents)
|
|
1623
|
+
SESSION_TEAM_INFO: "session:team-info",
|
|
1624
|
+
SESSION_TEAM_AGENT_DELTA: "session:team-agent-delta",
|
|
1625
|
+
// Chat history events
|
|
1626
|
+
CHAT_HISTORY_UPDATED: "chat:history-updated",
|
|
1627
|
+
CHAT_COMPACTING: "chat:compacting",
|
|
1628
|
+
CHAT_COMPACTED: "chat:compacted",
|
|
1629
|
+
// Conversation events (multi-conversation per agent)
|
|
1630
|
+
CONVERSATION_CREATED: "conversation:created",
|
|
1631
|
+
CONVERSATION_DELETED: "conversation:deleted",
|
|
1632
|
+
CONVERSATION_UPDATED: "conversation:updated",
|
|
1633
|
+
// rename / pin / active-change
|
|
1634
|
+
// Cron events
|
|
1635
|
+
CRON_JOB_ADDED: "cron:job-added",
|
|
1636
|
+
CRON_JOB_UPDATED: "cron:job-updated",
|
|
1637
|
+
CRON_JOB_REMOVED: "cron:job-removed",
|
|
1638
|
+
CRON_JOB_STARTED: "cron:job-started",
|
|
1639
|
+
CRON_JOB_FINISHED: "cron:job-finished",
|
|
1640
|
+
CRON_NOTIFICATION: "cron:notification",
|
|
1641
|
+
// Subagent events
|
|
1642
|
+
SUBAGENT_START: "subagent:start",
|
|
1643
|
+
SUBAGENT_SEND: "subagent:send",
|
|
1644
|
+
SUBAGENT_STARTED: "subagent:started",
|
|
1645
|
+
SUBAGENT_RESULT: "subagent:result",
|
|
1646
|
+
SUBAGENT_ERROR: "subagent:error",
|
|
1647
|
+
// Sync events
|
|
1648
|
+
SYNC_PULLED: "sync:pulled",
|
|
1649
|
+
SYNC_CONFLICT_RESOLVED: "sync:conflict-resolved",
|
|
1650
|
+
// Project registry events
|
|
1651
|
+
PROJECT_CREATED: "project:created",
|
|
1652
|
+
// Notes events
|
|
1653
|
+
NOTES_UPDATED: "notes:updated",
|
|
1654
|
+
NOTES_TREE_CHANGED: "notes:tree-changed",
|
|
1655
|
+
// Config events
|
|
1656
|
+
CONFIG_CHANGED: "config:changed",
|
|
1657
|
+
// Audio capture events
|
|
1658
|
+
AUDIO_STARTED: "audio:started",
|
|
1659
|
+
AUDIO_STOPPED: "audio:stopped",
|
|
1660
|
+
AUDIO_CHUNK_SAVED: "audio:chunk-saved",
|
|
1661
|
+
AUDIO_ERROR: "audio:error",
|
|
1662
|
+
AUDIO_TRANSCRIPTION_COMPLETE: "audio:transcription-complete",
|
|
1663
|
+
// System health events
|
|
1664
|
+
SYSTEM_HEALTH: "system:health",
|
|
1665
|
+
// Mobile client incidents (a freeze/crash line arrived in an uploaded iOS log)
|
|
1666
|
+
CLIENT_INCIDENT: "client:incident",
|
|
1667
|
+
// Cloud-companion setup job progress (never carries the pairing code)
|
|
1668
|
+
CLOUD_SETUP_UPDATE: "cloud-setup:update"
|
|
1669
|
+
};
|
|
1670
|
+
EVENT_HISTORY_SIZE = 200;
|
|
1671
|
+
eventHistory = [];
|
|
1672
|
+
KEY_BUS_EVENTS = /* @__PURE__ */ new Set([
|
|
1673
|
+
"session:start",
|
|
1674
|
+
"session:send",
|
|
1675
|
+
"session:started",
|
|
1676
|
+
"session:ended",
|
|
1677
|
+
"session:deleted",
|
|
1678
|
+
"session:content-updated",
|
|
1679
|
+
"session:result",
|
|
1680
|
+
"session:error",
|
|
1681
|
+
"session:status-changed",
|
|
1682
|
+
"subagent:start",
|
|
1683
|
+
"subagent:result",
|
|
1684
|
+
"subagent:error",
|
|
1685
|
+
"task:created",
|
|
1686
|
+
"task:updated",
|
|
1687
|
+
"task:completed",
|
|
1688
|
+
"task:deleted",
|
|
1689
|
+
"task:unblocked"
|
|
1690
|
+
]);
|
|
1691
|
+
EventBus = class {
|
|
1692
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
1693
|
+
/**
|
|
1694
|
+
* Register a named subscriber.
|
|
1695
|
+
* Events are delivered only when the subscriber's name is in the event's destinations
|
|
1696
|
+
* (or destinations includes "*"), unless `global: true` is set — then the subscriber
|
|
1697
|
+
* receives ALL events regardless of destinations.
|
|
1698
|
+
*/
|
|
1699
|
+
subscribe(name, handler, filter) {
|
|
1700
|
+
if (filter && typeof filter === "object" && ("global" in filter || "interest" in filter || "filter" in filter)) {
|
|
1701
|
+
this.subscribers.set(name, { name, handler, filter: filter.filter, global: filter.global, interest: filter.interest });
|
|
1702
|
+
} else {
|
|
1703
|
+
this.subscribers.set(name, { name, handler, filter });
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Remove a subscriber by name.
|
|
1708
|
+
*/
|
|
1709
|
+
unsubscribe(name) {
|
|
1710
|
+
this.subscribers.delete(name);
|
|
1711
|
+
}
|
|
1712
|
+
emit(name, data, destinations, options) {
|
|
1713
|
+
const event = {
|
|
1714
|
+
name,
|
|
1715
|
+
data,
|
|
1716
|
+
destinations,
|
|
1717
|
+
urgency: options?.urgency ?? "normal",
|
|
1718
|
+
timestamp: Date.now(),
|
|
1719
|
+
source: options?.source ?? "unknown",
|
|
1720
|
+
traceId: randomBytes(4).toString("hex"),
|
|
1721
|
+
...options?.reemit ? { reemit: true } : {}
|
|
1722
|
+
};
|
|
1723
|
+
eventHistory.push(event);
|
|
1724
|
+
if (eventHistory.length > EVENT_HISTORY_SIZE) {
|
|
1725
|
+
eventHistory.shift();
|
|
1726
|
+
}
|
|
1727
|
+
log.bus.debug(`emit ${name}`, { traceId: event.traceId, destinations, source: options?.source });
|
|
1728
|
+
if (KEY_BUS_EVENTS.has(name)) {
|
|
1729
|
+
log.bus.info(`emit ${name}`, { traceId: event.traceId, destinations, source: options?.source });
|
|
1730
|
+
}
|
|
1731
|
+
for (const [, subscriber] of this.subscribers) {
|
|
1732
|
+
if (subscriber.global && event.reemit) continue;
|
|
1733
|
+
if (!subscriber.global && !destinations.includes("*") && !destinations.includes(subscriber.name)) {
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (subscriber.global && subscriber.interest && !subscriber.interest.some((p) => name.startsWith(p))) {
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
if (subscriber.filter && !subscriber.filter(event)) {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
log.bus.debug("event delivered", { name, subscriber: subscriber.name, traceId: event.traceId });
|
|
1744
|
+
const result = subscriber.handler(event);
|
|
1745
|
+
if (result && typeof result.then === "function") {
|
|
1746
|
+
result.catch((err) => {
|
|
1747
|
+
log.bus.error(`subscriber "${subscriber.name}" threw on event "${name}" (async)`, {
|
|
1748
|
+
eventName: name,
|
|
1749
|
+
traceId: event.traceId,
|
|
1750
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1751
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
1752
|
+
});
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
} catch (err) {
|
|
1756
|
+
log.bus.error(`subscriber "${subscriber.name}" threw on event "${name}"`, {
|
|
1757
|
+
eventName: name,
|
|
1758
|
+
traceId: event.traceId,
|
|
1759
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1760
|
+
stack: err instanceof Error ? err.stack : void 0
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Check if a subscriber is registered.
|
|
1767
|
+
*/
|
|
1768
|
+
has(name) {
|
|
1769
|
+
return this.subscribers.has(name);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Remove all subscribers.
|
|
1773
|
+
*/
|
|
1774
|
+
clear() {
|
|
1775
|
+
this.subscribers.clear();
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
bus = new EventBus();
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// src/core/phase.ts
|
|
1783
|
+
function deriveStatusFromPhase(phase) {
|
|
1784
|
+
return PHASE_TO_STATUS[phase] ?? "todo";
|
|
1785
|
+
}
|
|
1786
|
+
var PHASE_TO_STATUS, PHASE_ORDER, VALID_PHASES;
|
|
1787
|
+
var init_phase = __esm({
|
|
1788
|
+
"src/core/phase.ts"() {
|
|
1789
|
+
"use strict";
|
|
1790
|
+
init_logging();
|
|
1791
|
+
PHASE_TO_STATUS = {
|
|
1792
|
+
TODO: "todo",
|
|
1793
|
+
IN_PROGRESS: "in_progress",
|
|
1794
|
+
AGENT_COMPLETE: "in_progress",
|
|
1795
|
+
AWAIT_HUMAN_ACTION: "in_progress",
|
|
1796
|
+
HUMAN_VERIFIED: "in_progress",
|
|
1797
|
+
POST_WORK_COMPLETED: "in_progress",
|
|
1798
|
+
COMPLETE: "done"
|
|
1799
|
+
};
|
|
1800
|
+
PHASE_ORDER = [
|
|
1801
|
+
"TODO",
|
|
1802
|
+
"IN_PROGRESS",
|
|
1803
|
+
"AGENT_COMPLETE",
|
|
1804
|
+
"AWAIT_HUMAN_ACTION",
|
|
1805
|
+
"HUMAN_VERIFIED",
|
|
1806
|
+
"POST_WORK_COMPLETED",
|
|
1807
|
+
"COMPLETE"
|
|
1808
|
+
];
|
|
1809
|
+
VALID_PHASES = new Set(PHASE_ORDER);
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
// src/utils/format.ts
|
|
1814
|
+
import crypto2 from "crypto";
|
|
1815
|
+
var init_format = __esm({
|
|
1816
|
+
"src/utils/format.ts"() {
|
|
1817
|
+
"use strict";
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
// src/utils/file-ops.ts
|
|
1822
|
+
import crypto3 from "crypto";
|
|
1823
|
+
import fsp3 from "fs/promises";
|
|
1824
|
+
import path6 from "path";
|
|
1825
|
+
var MAX_READ_SIZE;
|
|
1826
|
+
var init_file_ops = __esm({
|
|
1827
|
+
"src/utils/file-ops.ts"() {
|
|
1828
|
+
"use strict";
|
|
1829
|
+
init_file_lock();
|
|
1830
|
+
MAX_READ_SIZE = 10 * 1024 * 1024;
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
// src/core/memory-file.ts
|
|
1835
|
+
import fs7 from "fs";
|
|
1836
|
+
import path7 from "path";
|
|
1837
|
+
function resolveMemoryPath(agentId) {
|
|
1838
|
+
if (!agentId || agentId === "general") return MEMORY_FILE;
|
|
1839
|
+
return path7.join(agentMemoryDir(agentId), "MEMORY.md");
|
|
1840
|
+
}
|
|
1841
|
+
function ensureMemoryFile(agentId) {
|
|
1842
|
+
const filePath = resolveMemoryPath(agentId);
|
|
1843
|
+
if (fs7.existsSync(filePath)) return;
|
|
1844
|
+
fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
1845
|
+
fs7.writeFileSync(filePath, DEFAULT_TEMPLATE, "utf-8");
|
|
1846
|
+
}
|
|
1847
|
+
var DEFAULT_TEMPLATE;
|
|
1848
|
+
var init_memory_file = __esm({
|
|
1849
|
+
"src/core/memory-file.ts"() {
|
|
1850
|
+
"use strict";
|
|
1851
|
+
init_logging();
|
|
1852
|
+
init_constants();
|
|
1853
|
+
init_file_ops();
|
|
1854
|
+
DEFAULT_TEMPLATE = `---
|
|
1855
|
+
name: Global Memory
|
|
1856
|
+
description: >
|
|
1857
|
+
Curated knowledge and preferences. Updated by the agent as it learns.
|
|
1858
|
+
---
|
|
1859
|
+
`;
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
// src/core/tmp-sweep.ts
|
|
1864
|
+
import fsp4 from "fs/promises";
|
|
1865
|
+
import path8 from "path";
|
|
1866
|
+
function sweepDirs() {
|
|
1867
|
+
return [
|
|
1868
|
+
WALNUT_HOME,
|
|
1869
|
+
SYNC_DIR,
|
|
1870
|
+
path8.join(WALNUT_HOME, "sessions"),
|
|
1871
|
+
path8.join(WALNUT_HOME, "sessions", "transcripts"),
|
|
1872
|
+
TASKS_DIR,
|
|
1873
|
+
path8.join(TASKS_DIR, "outbox"),
|
|
1874
|
+
CONVERSATIONS_DIR,
|
|
1875
|
+
MEMORY_DIR,
|
|
1876
|
+
DAILY_DIR,
|
|
1877
|
+
// Projection cache (Phase 3) — gitignored, but a crashed rename still
|
|
1878
|
+
// leaves an orphan the read path never cleans up.
|
|
1879
|
+
PROJECTION_CACHE_DIR,
|
|
1880
|
+
TRANSCRIPT_CACHE_DIR,
|
|
1881
|
+
// Offline task-op queue (Phase 4) — same story: gitignored, but a torn
|
|
1882
|
+
// write leaves a .tmp the flush loop skips forever.
|
|
1883
|
+
TASK_QUEUE_DIR
|
|
1884
|
+
];
|
|
1885
|
+
}
|
|
1886
|
+
async function sweepOrphanAtomicTmpFiles() {
|
|
1887
|
+
const removed = [];
|
|
1888
|
+
const cutoff = Date.now() - MIN_AGE_MS;
|
|
1889
|
+
for (const dir of sweepDirs()) {
|
|
1890
|
+
try {
|
|
1891
|
+
const entries = await fsp4.readdir(dir, { withFileTypes: true });
|
|
1892
|
+
for (const entry of entries) {
|
|
1893
|
+
if (!entry.isFile() || !ORPHAN_RE.test(entry.name)) continue;
|
|
1894
|
+
const full = path8.join(dir, entry.name);
|
|
1895
|
+
try {
|
|
1896
|
+
const st = await fsp4.stat(full);
|
|
1897
|
+
if (st.mtimeMs > cutoff) continue;
|
|
1898
|
+
await fsp4.unlink(full);
|
|
1899
|
+
removed.push(full);
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (removed.length > 0) {
|
|
1907
|
+
log.web.info("swept orphaned atomic-write temp files", {
|
|
1908
|
+
count: removed.length,
|
|
1909
|
+
files: removed.slice(0, 10)
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
return removed;
|
|
1913
|
+
}
|
|
1914
|
+
var ORPHAN_RE, MIN_AGE_MS;
|
|
1915
|
+
var init_tmp_sweep = __esm({
|
|
1916
|
+
"src/core/tmp-sweep.ts"() {
|
|
1917
|
+
"use strict";
|
|
1918
|
+
init_constants();
|
|
1919
|
+
init_logging();
|
|
1920
|
+
ORPHAN_RE = /^\.open-walnut-[0-9a-f]+\.tmp$/;
|
|
1921
|
+
MIN_AGE_MS = 60 * 60 * 1e3;
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// src/core/init.ts
|
|
1926
|
+
import fsp5 from "fs/promises";
|
|
1927
|
+
import fs8 from "fs";
|
|
1928
|
+
async function migrateLegacyMemoryFile() {
|
|
1929
|
+
try {
|
|
1930
|
+
if (fs8.existsSync(LEGACY_MEMORY_FILE) && !fs8.existsSync(MEMORY_FILE)) {
|
|
1931
|
+
await fsp5.rename(LEGACY_MEMORY_FILE, MEMORY_FILE);
|
|
1932
|
+
log.memory.info("Migrated MEMORY.md into memory/ folder", { from: LEGACY_MEMORY_FILE, to: MEMORY_FILE });
|
|
1933
|
+
}
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
log.memory.warn("MEMORY.md migration failed (will retry next start)", {
|
|
1936
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
async function initDirectories() {
|
|
1941
|
+
await ensureDir(WALNUT_HOME);
|
|
1942
|
+
await ensureDir(TASKS_DIR);
|
|
1943
|
+
await ensureDir(ARCHIVE_DIR);
|
|
1944
|
+
await ensureDir(MEMORY_DIR);
|
|
1945
|
+
await ensureDir(SESSIONS_DIR);
|
|
1946
|
+
await ensureDir(SYNC_DIR);
|
|
1947
|
+
await ensureDir(DAILY_DIR);
|
|
1948
|
+
await ensureDir(REPOS_MEMORY_DIR);
|
|
1949
|
+
await ensureDir(COMPACTION_DIR);
|
|
1950
|
+
await ensureDir(NOTES_DIR);
|
|
1951
|
+
await ensureDir(REPOSITORIES_DIR);
|
|
1952
|
+
await ensureDir(TIMELINE_DIR);
|
|
1953
|
+
await ensureDir(TMP_DIR);
|
|
1954
|
+
await ensureDir(RECORDINGS_DIR);
|
|
1955
|
+
await ensureDir(CONFIG_SHARE_DIR);
|
|
1956
|
+
await ensureDir(COMMANDS_DIR);
|
|
1957
|
+
await ensureDir(GLOBAL_SKILLS_DIR);
|
|
1958
|
+
await ensureDir(SESSION_STREAMS_DIR);
|
|
1959
|
+
await ensureDir(IMAGES_DIR);
|
|
1960
|
+
await ensureDir(REMOTE_IMAGES_DIR);
|
|
1961
|
+
await migrateLegacyMemoryFile();
|
|
1962
|
+
ensureMemoryFile();
|
|
1963
|
+
await seedConfigDefaults();
|
|
1964
|
+
void sweepOrphanAtomicTmpFiles();
|
|
1965
|
+
}
|
|
1966
|
+
var init_init = __esm({
|
|
1967
|
+
"src/core/init.ts"() {
|
|
1968
|
+
"use strict";
|
|
1969
|
+
init_fs();
|
|
1970
|
+
init_constants();
|
|
1971
|
+
init_memory_file();
|
|
1972
|
+
init_config_manager();
|
|
1973
|
+
init_tmp_sweep();
|
|
1974
|
+
init_logging();
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
// src/core/integration-registry.ts
|
|
1979
|
+
var noopLocalSync, IntegrationRegistry, registry;
|
|
1980
|
+
var init_integration_registry = __esm({
|
|
1981
|
+
"src/core/integration-registry.ts"() {
|
|
1982
|
+
"use strict";
|
|
1983
|
+
noopLocalSync = {
|
|
1984
|
+
createTask: async () => null,
|
|
1985
|
+
deleteTask: async () => {
|
|
1986
|
+
},
|
|
1987
|
+
updateTitle: async () => {
|
|
1988
|
+
},
|
|
1989
|
+
updateDescription: async () => {
|
|
1990
|
+
},
|
|
1991
|
+
updateSummary: async () => {
|
|
1992
|
+
},
|
|
1993
|
+
updateNote: async () => {
|
|
1994
|
+
},
|
|
1995
|
+
updateConversationLog: async () => {
|
|
1996
|
+
},
|
|
1997
|
+
updatePriority: async () => {
|
|
1998
|
+
},
|
|
1999
|
+
updatePhase: async () => {
|
|
2000
|
+
},
|
|
2001
|
+
updateDueDate: async () => {
|
|
2002
|
+
},
|
|
2003
|
+
updateStar: async () => {
|
|
2004
|
+
},
|
|
2005
|
+
updateProject: async () => {
|
|
2006
|
+
},
|
|
2007
|
+
updateDependencies: async () => {
|
|
2008
|
+
},
|
|
2009
|
+
associateSubtask: async () => {
|
|
2010
|
+
},
|
|
2011
|
+
disassociateSubtask: async () => {
|
|
2012
|
+
},
|
|
2013
|
+
pushTask: async () => ({ serverTimestamp: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
2014
|
+
syncPoll: async () => {
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
IntegrationRegistry = class {
|
|
2018
|
+
plugins = /* @__PURE__ */ new Map();
|
|
2019
|
+
/** Register a plugin. Throws if duplicate ID. */
|
|
2020
|
+
register(id, plugin) {
|
|
2021
|
+
if (this.plugins.has(id)) {
|
|
2022
|
+
throw new Error(`Plugin "${id}" is already registered.`);
|
|
2023
|
+
}
|
|
2024
|
+
this.plugins.set(id, plugin);
|
|
2025
|
+
}
|
|
2026
|
+
/** Get a plugin by ID. Returns undefined if not found. */
|
|
2027
|
+
get(id) {
|
|
2028
|
+
return this.plugins.get(id);
|
|
2029
|
+
}
|
|
2030
|
+
/** Get all registered plugins. */
|
|
2031
|
+
getAll() {
|
|
2032
|
+
return [...this.plugins.values()];
|
|
2033
|
+
}
|
|
2034
|
+
/** Check if a plugin is registered. */
|
|
2035
|
+
has(id) {
|
|
2036
|
+
return this.plugins.has(id);
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Find the plugin that claims ownership of a project.
|
|
2040
|
+
* Iterates claims by priority (highest first). First match wins.
|
|
2041
|
+
* Always returns something — 'local' plugin is the universal fallback (priority -1).
|
|
2042
|
+
*
|
|
2043
|
+
* Inbox (the empty project) is structurally unclaimable, so callers never ask
|
|
2044
|
+
* about it — it is always 'local'.
|
|
2045
|
+
*/
|
|
2046
|
+
async getForProject(project) {
|
|
2047
|
+
const claimable = [...this.plugins.values()].filter((p) => p.claim).sort((a, b) => b.claim.priority - a.claim.priority);
|
|
2048
|
+
for (const plugin of claimable) {
|
|
2049
|
+
const claimed = await plugin.claim.fn(project);
|
|
2050
|
+
if (claimed) return plugin;
|
|
2051
|
+
}
|
|
2052
|
+
const local = this.plugins.get("local");
|
|
2053
|
+
if (local) return local;
|
|
2054
|
+
throw new Error("No plugin registered for project and no local fallback found.");
|
|
2055
|
+
}
|
|
2056
|
+
/** Remove all plugins (useful for testing). Re-registers the local fallback. */
|
|
2057
|
+
clear() {
|
|
2058
|
+
this.plugins.clear();
|
|
2059
|
+
this.ensureLocalFallback();
|
|
2060
|
+
}
|
|
2061
|
+
/** Ensure the local fallback plugin is registered. Called at init and after clear(). */
|
|
2062
|
+
ensureLocalFallback() {
|
|
2063
|
+
if (this.plugins.has("local")) return;
|
|
2064
|
+
this.plugins.set("local", {
|
|
2065
|
+
id: "local",
|
|
2066
|
+
name: "Local (fallback)",
|
|
2067
|
+
config: {},
|
|
2068
|
+
sync: noopLocalSync,
|
|
2069
|
+
claim: { fn: (() => true), priority: -1 },
|
|
2070
|
+
migrations: [],
|
|
2071
|
+
httpRoutes: []
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
registry = new IntegrationRegistry();
|
|
2076
|
+
registry.ensureLocalFallback();
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
// src/core/task-db.ts
|
|
2081
|
+
import Database from "better-sqlite3";
|
|
2082
|
+
import fs9 from "fs";
|
|
2083
|
+
import path9 from "path";
|
|
2084
|
+
import yaml2 from "js-yaml";
|
|
2085
|
+
function getDb() {
|
|
2086
|
+
if (db) return db;
|
|
2087
|
+
if (initAttempted) throw initError;
|
|
2088
|
+
initAttempted = true;
|
|
2089
|
+
try {
|
|
2090
|
+
fs9.mkdirSync(path9.dirname(TASK_DB_PATH), { recursive: true });
|
|
2091
|
+
const handle = new Database(TASK_DB_PATH);
|
|
2092
|
+
handle.pragma("journal_mode = WAL");
|
|
2093
|
+
handle.pragma("busy_timeout = 5000");
|
|
2094
|
+
handle.pragma("wal_autocheckpoint = 1000");
|
|
2095
|
+
handle.pragma("synchronous = NORMAL");
|
|
2096
|
+
handle.pragma("foreign_keys = ON");
|
|
2097
|
+
handle.exec(SCHEMA_SQL);
|
|
2098
|
+
runOneTimeMigrations(handle);
|
|
2099
|
+
let checkpoint = null;
|
|
2100
|
+
try {
|
|
2101
|
+
checkpoint = handle.pragma("wal_checkpoint(TRUNCATE)");
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
log.task.warn("task-db: WAL checkpoint on open failed", { err: String(err) });
|
|
2104
|
+
}
|
|
2105
|
+
db = handle;
|
|
2106
|
+
log.task.info("task-db opened", { path: TASK_DB_PATH });
|
|
2107
|
+
log.task.info("task-db: WAL checkpoint on open", { result: checkpoint });
|
|
2108
|
+
return db;
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
initError = err;
|
|
2111
|
+
log.task.error("task-db open failed", { path: TASK_DB_PATH, err: String(err) });
|
|
2112
|
+
throw err;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
function transaction(fn) {
|
|
2116
|
+
const handle = getDb();
|
|
2117
|
+
if (!handle) {
|
|
2118
|
+
throw new Error("task-db: transaction() called before database was successfully opened");
|
|
2119
|
+
}
|
|
2120
|
+
const tx = handle.transaction(fn);
|
|
2121
|
+
return tx(handle);
|
|
2122
|
+
}
|
|
2123
|
+
function rowToTask(row) {
|
|
2124
|
+
let task = {};
|
|
2125
|
+
if (row.payload != null && row.payload !== "") {
|
|
2126
|
+
try {
|
|
2127
|
+
const parsed = JSON.parse(row.payload);
|
|
2128
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2129
|
+
task = { ...parsed };
|
|
2130
|
+
}
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
log.task.warn("rowToTask: payload JSON parse failed", { id: row.id, err: String(err) });
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
for (const col of EXPLICIT_TASK_COLUMNS) {
|
|
2136
|
+
const val = row[col];
|
|
2137
|
+
if (val === void 0 || val === null) continue;
|
|
2138
|
+
if (JSON_COLUMNS.has(col)) {
|
|
2139
|
+
if (val === "") continue;
|
|
2140
|
+
try {
|
|
2141
|
+
task[col] = JSON.parse(val);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
log.task.warn("rowToTask: JSON column parse failed", { id: row.id, col, err: String(err) });
|
|
2144
|
+
}
|
|
2145
|
+
} else if (col === "pinned") {
|
|
2146
|
+
task.pinned = val === 1 || val === true;
|
|
2147
|
+
} else {
|
|
2148
|
+
task[col] = val;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (row._synced_at != null && row._synced_at !== "") {
|
|
2152
|
+
task._syncedAt = row._synced_at;
|
|
2153
|
+
}
|
|
2154
|
+
if (typeof task.project !== "string") task.project = "";
|
|
2155
|
+
if (typeof task.session_ids === "undefined") task.session_ids = [];
|
|
2156
|
+
if (typeof task.description === "undefined") task.description = "";
|
|
2157
|
+
if (typeof task.summary === "undefined") task.summary = "";
|
|
2158
|
+
if (typeof task.note === "undefined") task.note = "";
|
|
2159
|
+
return task;
|
|
2160
|
+
}
|
|
2161
|
+
function taskToRow(task) {
|
|
2162
|
+
const row = {};
|
|
2163
|
+
for (const col of EXPLICIT_TASK_COLUMNS) {
|
|
2164
|
+
if (col === "_synced_at") continue;
|
|
2165
|
+
if (!(col in task)) continue;
|
|
2166
|
+
const val = task[col];
|
|
2167
|
+
if (val === void 0) continue;
|
|
2168
|
+
if (JSON_COLUMNS.has(col)) {
|
|
2169
|
+
row[col] = val === null ? null : JSON.stringify(val);
|
|
2170
|
+
} else if (col === "pinned") {
|
|
2171
|
+
row.pinned = val ? 1 : 0;
|
|
2172
|
+
} else {
|
|
2173
|
+
row[col] = val;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if ("_syncedAt" in task) {
|
|
2177
|
+
row._synced_at = task._syncedAt ?? null;
|
|
2178
|
+
}
|
|
2179
|
+
const payload = {};
|
|
2180
|
+
let hasPayload = false;
|
|
2181
|
+
for (const key of Object.keys(task)) {
|
|
2182
|
+
if (EXPLICIT_TASK_KEYS.has(key)) continue;
|
|
2183
|
+
if (RETIRED_TASK_KEYS.has(key)) continue;
|
|
2184
|
+
const val = task[key];
|
|
2185
|
+
if (val === void 0) continue;
|
|
2186
|
+
payload[key] = val;
|
|
2187
|
+
hasPayload = true;
|
|
2188
|
+
}
|
|
2189
|
+
if (hasPayload) {
|
|
2190
|
+
row.payload = JSON.stringify(payload);
|
|
2191
|
+
} else if ("payload" in task) {
|
|
2192
|
+
row.payload = null;
|
|
2193
|
+
}
|
|
2194
|
+
return row;
|
|
2195
|
+
}
|
|
2196
|
+
function runOneTimeMigrations(handle) {
|
|
2197
|
+
const current = handle.pragma("user_version", { simple: true });
|
|
2198
|
+
if (current >= SCHEMA_VERSION) return;
|
|
2199
|
+
if (current < 2) {
|
|
2200
|
+
const staleIndexes = handle.prepare(
|
|
2201
|
+
"SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_tasks_ext_%'"
|
|
2202
|
+
).all();
|
|
2203
|
+
for (const { name } of staleIndexes) {
|
|
2204
|
+
handle.exec(`DROP INDEX IF EXISTS "${name}";`);
|
|
2205
|
+
}
|
|
2206
|
+
handle.exec(`DROP TABLE IF EXISTS task_projects;`);
|
|
2207
|
+
}
|
|
2208
|
+
if (current < 3) {
|
|
2209
|
+
const cols = handle.prepare(`PRAGMA table_info(task_groups)`).all();
|
|
2210
|
+
if (!cols.some((c) => c.name === "hidden")) {
|
|
2211
|
+
handle.exec(`ALTER TABLE task_groups ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;`);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
if (current < 4) {
|
|
2215
|
+
const cols = handle.prepare(`PRAGMA table_info(tasks)`).all();
|
|
2216
|
+
if (!cols.some((c) => c.name === "start_date")) {
|
|
2217
|
+
handle.exec(`ALTER TABLE tasks ADD COLUMN start_date TEXT;`);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (current < 5) {
|
|
2221
|
+
migrateToProjectOnly(handle);
|
|
2222
|
+
}
|
|
2223
|
+
if (current < 6) {
|
|
2224
|
+
migrateReadMarkerToUnread(handle);
|
|
2225
|
+
}
|
|
2226
|
+
handle.pragma("user_version = " + SCHEMA_VERSION);
|
|
2227
|
+
}
|
|
2228
|
+
function legacyListName(category, project) {
|
|
2229
|
+
if (!category || !project || category.toLowerCase() === project.toLowerCase()) {
|
|
2230
|
+
return category || project;
|
|
2231
|
+
}
|
|
2232
|
+
return `${category} / ${project}`;
|
|
2233
|
+
}
|
|
2234
|
+
function pickMajoritySource(weights, projectName) {
|
|
2235
|
+
const ranked = [...weights.entries()].filter(([src, n]) => src && src !== "local" && n > 0).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
2236
|
+
if (ranked.length === 0) return "local";
|
|
2237
|
+
if (ranked.length === 1) return ranked[0][0];
|
|
2238
|
+
if (ranked[0][1] > ranked[1][1]) {
|
|
2239
|
+
log.task.warn("task-db: project merged categories from several providers, majority wins", {
|
|
2240
|
+
project: projectName,
|
|
2241
|
+
source: ranked[0][0],
|
|
2242
|
+
weights: Object.fromEntries(ranked)
|
|
2243
|
+
});
|
|
2244
|
+
return ranked[0][0];
|
|
2245
|
+
}
|
|
2246
|
+
log.task.warn("task-db: project merged categories with tied sources, using local", {
|
|
2247
|
+
project: projectName,
|
|
2248
|
+
weights: Object.fromEntries(ranked)
|
|
2249
|
+
});
|
|
2250
|
+
return "local";
|
|
2251
|
+
}
|
|
2252
|
+
function parseYamlObject(text) {
|
|
2253
|
+
if (!text || !text.trim()) return null;
|
|
2254
|
+
try {
|
|
2255
|
+
const parsed = yaml2.load(text);
|
|
2256
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2257
|
+
return parsed;
|
|
2258
|
+
}
|
|
2259
|
+
} catch (err) {
|
|
2260
|
+
log.task.warn("task-db v5: metadata YAML parse failed", { err: String(err) });
|
|
2261
|
+
}
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
function mergeFirstWins(target, src, context) {
|
|
2265
|
+
for (const [key, value] of Object.entries(src)) {
|
|
2266
|
+
if (key in target) {
|
|
2267
|
+
if (JSON.stringify(target[key]) !== JSON.stringify(value)) {
|
|
2268
|
+
log.task.warn("task-db v5: metadata conflict, keeping first value", { context, key });
|
|
2269
|
+
}
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
target[key] = value;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function promoteLegacyGroup(category, project) {
|
|
2276
|
+
const cat = (category ?? "").trim();
|
|
2277
|
+
const proj = (project ?? "").trim();
|
|
2278
|
+
if (proj.toLowerCase() === "quick start") return "";
|
|
2279
|
+
if (!proj || proj.toLowerCase() === cat.toLowerCase()) {
|
|
2280
|
+
if (!cat || cat.toLowerCase() === "inbox") return "";
|
|
2281
|
+
return cat;
|
|
2282
|
+
}
|
|
2283
|
+
return proj;
|
|
2284
|
+
}
|
|
2285
|
+
function migrateToProjectOnly(handle) {
|
|
2286
|
+
handle.exec(TASK_PROJECTS_DDL);
|
|
2287
|
+
const cols = handle.prepare(`PRAGMA table_info(tasks)`).all();
|
|
2288
|
+
if (!cols.some((c) => c.name === "category")) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
try {
|
|
2292
|
+
handle.pragma("wal_checkpoint(TRUNCATE)");
|
|
2293
|
+
if (!fs9.existsSync(TASK_DB_PRE_V5_BACKUP_PATH)) {
|
|
2294
|
+
fs9.copyFileSync(TASK_DB_PATH, TASK_DB_PRE_V5_BACKUP_PATH);
|
|
2295
|
+
log.task.info("task-db v5: wrote pre-migration backup", { path: TASK_DB_PRE_V5_BACKUP_PATH });
|
|
2296
|
+
}
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
log.task.warn("task-db v5: backup failed (continuing)", {
|
|
2299
|
+
path: TASK_DB_PRE_V5_BACKUP_PATH,
|
|
2300
|
+
err: String(err)
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
const summary = handle.transaction(() => {
|
|
2304
|
+
const sentinels = handle.prepare(
|
|
2305
|
+
`SELECT category, project, title, description FROM tasks
|
|
2306
|
+
WHERE title IN ('.metadata_project', '.metadata_category')`
|
|
2307
|
+
).all();
|
|
2308
|
+
const catMeta = /* @__PURE__ */ new Map();
|
|
2309
|
+
const projMeta = /* @__PURE__ */ new Map();
|
|
2310
|
+
const sentinelPairs = [];
|
|
2311
|
+
for (const row of sentinels) {
|
|
2312
|
+
const cat = (row.category ?? "").trim().toLowerCase();
|
|
2313
|
+
const settings = parseYamlObject(row.description);
|
|
2314
|
+
if (row.title === ".metadata_category") {
|
|
2315
|
+
if (!settings) continue;
|
|
2316
|
+
const bucket2 = catMeta.get(cat) ?? {};
|
|
2317
|
+
mergeFirstWins(bucket2, settings, `category:${cat}`);
|
|
2318
|
+
catMeta.set(cat, bucket2);
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
sentinelPairs.push({ category: row.category, project: row.project });
|
|
2322
|
+
if (!settings) continue;
|
|
2323
|
+
const key = `${cat}\0${(row.project ?? "").trim().toLowerCase()}`;
|
|
2324
|
+
const bucket = projMeta.get(key) ?? {};
|
|
2325
|
+
mergeFirstWins(bucket, settings, `project:${key}`);
|
|
2326
|
+
projMeta.set(key, bucket);
|
|
2327
|
+
}
|
|
2328
|
+
handle.prepare(`DELETE FROM tasks WHERE title IN ('.metadata_project', '.metadata_category')`).run();
|
|
2329
|
+
const groups = handle.prepare(`SELECT category, project, COUNT(*) AS n FROM tasks GROUP BY category, project`).all().map((r) => ({
|
|
2330
|
+
category: r.category,
|
|
2331
|
+
project: r.project,
|
|
2332
|
+
taskCount: r.n,
|
|
2333
|
+
target: promoteLegacyGroup(r.category ?? "", r.project ?? ""),
|
|
2334
|
+
final: ""
|
|
2335
|
+
}));
|
|
2336
|
+
for (const pair of sentinelPairs) {
|
|
2337
|
+
const exists = groups.some((g) => g.category === pair.category && g.project === pair.project);
|
|
2338
|
+
if (exists) continue;
|
|
2339
|
+
groups.push({
|
|
2340
|
+
category: pair.category,
|
|
2341
|
+
project: pair.project,
|
|
2342
|
+
taskCount: 0,
|
|
2343
|
+
target: promoteLegacyGroup(pair.category ?? "", pair.project ?? ""),
|
|
2344
|
+
final: ""
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
const spellings = /* @__PURE__ */ new Map();
|
|
2348
|
+
for (const g of groups) {
|
|
2349
|
+
if (!g.target) continue;
|
|
2350
|
+
const lower = g.target.toLowerCase();
|
|
2351
|
+
const bySpelling = spellings.get(lower) ?? /* @__PURE__ */ new Map();
|
|
2352
|
+
bySpelling.set(g.target, (bySpelling.get(g.target) ?? 0) + g.taskCount);
|
|
2353
|
+
spellings.set(lower, bySpelling);
|
|
2354
|
+
}
|
|
2355
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
2356
|
+
for (const [lower, bySpelling] of spellings) {
|
|
2357
|
+
const best = [...bySpelling.entries()].sort(
|
|
2358
|
+
(a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
|
|
2359
|
+
)[0][0];
|
|
2360
|
+
canonical.set(lower, best);
|
|
2361
|
+
}
|
|
2362
|
+
for (const g of groups) {
|
|
2363
|
+
g.final = g.target ? canonical.get(g.target.toLowerCase()) ?? g.target : "";
|
|
2364
|
+
}
|
|
2365
|
+
const updateGroup = handle.prepare(
|
|
2366
|
+
`UPDATE tasks SET project = @project WHERE category IS @category AND project IS @old`
|
|
2367
|
+
);
|
|
2368
|
+
for (const g of groups) {
|
|
2369
|
+
if ((g.project ?? "") === g.final) continue;
|
|
2370
|
+
updateGroup.run({ project: g.final, category: g.category, old: g.project });
|
|
2371
|
+
}
|
|
2372
|
+
let legacyCatSources = /* @__PURE__ */ new Map();
|
|
2373
|
+
let catOrder = [];
|
|
2374
|
+
try {
|
|
2375
|
+
const rows = handle.prepare(
|
|
2376
|
+
`SELECT name, source FROM task_categories
|
|
2377
|
+
ORDER BY (order_index IS NULL), order_index, name`
|
|
2378
|
+
).all();
|
|
2379
|
+
catOrder = rows.map((r) => r.name);
|
|
2380
|
+
legacyCatSources = new Map(rows.map((r) => [r.name.trim().toLowerCase(), r.source || "local"]));
|
|
2381
|
+
} catch {
|
|
2382
|
+
}
|
|
2383
|
+
const contributors = /* @__PURE__ */ new Map();
|
|
2384
|
+
for (const g of groups) {
|
|
2385
|
+
if (!g.final) continue;
|
|
2386
|
+
const key = g.final.toLowerCase();
|
|
2387
|
+
const list = contributors.get(key) ?? [];
|
|
2388
|
+
list.push(g);
|
|
2389
|
+
contributors.set(key, list);
|
|
2390
|
+
}
|
|
2391
|
+
const orderedKeys = [];
|
|
2392
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2393
|
+
const catsInOrder = [
|
|
2394
|
+
...catOrder,
|
|
2395
|
+
...[...new Set(groups.map((g) => (g.category ?? "").trim()))].filter((c) => !catOrder.some((k) => k.trim().toLowerCase() === c.toLowerCase())).sort()
|
|
2396
|
+
];
|
|
2397
|
+
for (const cat of catsInOrder) {
|
|
2398
|
+
const names = [
|
|
2399
|
+
...new Set(
|
|
2400
|
+
groups.filter((g) => g.final && (g.category ?? "").trim().toLowerCase() === cat.trim().toLowerCase()).map((g) => g.final)
|
|
2401
|
+
)
|
|
2402
|
+
].sort();
|
|
2403
|
+
for (const name of names) {
|
|
2404
|
+
const key = name.toLowerCase();
|
|
2405
|
+
if (seen.has(key)) continue;
|
|
2406
|
+
seen.add(key);
|
|
2407
|
+
orderedKeys.push(key);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
for (const key of [...contributors.keys()].sort()) {
|
|
2411
|
+
if (!seen.has(key)) {
|
|
2412
|
+
seen.add(key);
|
|
2413
|
+
orderedKeys.push(key);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
const insertProject = handle.prepare(
|
|
2417
|
+
`INSERT INTO task_projects (name, source, order_index, metadata)
|
|
2418
|
+
VALUES (@name, @source, @order_index, @metadata)
|
|
2419
|
+
ON CONFLICT(name) DO NOTHING`
|
|
2420
|
+
);
|
|
2421
|
+
const selectMinority = handle.prepare(
|
|
2422
|
+
`SELECT id, source FROM tasks WHERE project = @name COLLATE NOCASE AND source IS NOT @source`
|
|
2423
|
+
);
|
|
2424
|
+
const normalizeMinority = handle.prepare(
|
|
2425
|
+
`UPDATE tasks SET source = @source, ext = NULL, sync_error = NULL, updated_at = @now,
|
|
2426
|
+
payload = CASE WHEN payload IS NOT NULL AND json_valid(payload)
|
|
2427
|
+
THEN json_remove(payload, '$.external_url') ELSE payload END
|
|
2428
|
+
WHERE id = @id`
|
|
2429
|
+
);
|
|
2430
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2431
|
+
let normalizedMinorityTotal = 0;
|
|
2432
|
+
const perProject = [];
|
|
2433
|
+
orderedKeys.forEach((key, index) => {
|
|
2434
|
+
const groupsForProject = contributors.get(key);
|
|
2435
|
+
if (!groupsForProject || groupsForProject.length === 0) return;
|
|
2436
|
+
const name = groupsForProject[0].final;
|
|
2437
|
+
const taskCount = groupsForProject.reduce((sum, g) => sum + g.taskCount, 0);
|
|
2438
|
+
const providerWeights = /* @__PURE__ */ new Map();
|
|
2439
|
+
for (const g of groupsForProject) {
|
|
2440
|
+
const src = legacyCatSources.get((g.category ?? "").trim().toLowerCase()) ?? "local";
|
|
2441
|
+
if (!src || src === "local") continue;
|
|
2442
|
+
providerWeights.set(src, (providerWeights.get(src) ?? 0) + g.taskCount);
|
|
2443
|
+
}
|
|
2444
|
+
const source = pickMajoritySource(providerWeights, name);
|
|
2445
|
+
const base = {};
|
|
2446
|
+
const overlay = {};
|
|
2447
|
+
for (const g of groupsForProject) {
|
|
2448
|
+
const cat = (g.category ?? "").trim().toLowerCase();
|
|
2449
|
+
const fromCat = catMeta.get(cat);
|
|
2450
|
+
if (fromCat) mergeFirstWins(base, fromCat, `${name}<-category:${cat}`);
|
|
2451
|
+
const fromProj = projMeta.get(`${cat}\0${(g.project ?? "").trim().toLowerCase()}`);
|
|
2452
|
+
if (fromProj) mergeFirstWins(overlay, fromProj, `${name}<-project:${cat}/${g.project ?? ""}`);
|
|
2453
|
+
}
|
|
2454
|
+
const metadata = { ...base, ...overlay };
|
|
2455
|
+
const legacyCats = [
|
|
2456
|
+
...new Set(groupsForProject.map((g) => (g.category ?? "").trim()).filter(Boolean))
|
|
2457
|
+
].sort();
|
|
2458
|
+
if (legacyCats.length === 1) metadata.legacy_category = legacyCats[0];
|
|
2459
|
+
else if (legacyCats.length > 1) metadata.legacy_category = legacyCats;
|
|
2460
|
+
if (source === "ms-todo") {
|
|
2461
|
+
const owned = groupsForProject.filter((g) => (legacyCatSources.get((g.category ?? "").trim().toLowerCase()) ?? "local") === source).sort((a, b) => b.taskCount - a.taskCount);
|
|
2462
|
+
const pick = owned[0];
|
|
2463
|
+
if (pick) {
|
|
2464
|
+
const oldList = legacyListName((pick.category ?? "").trim(), (pick.project ?? "").trim());
|
|
2465
|
+
if (oldList && oldList !== name) metadata.remote_list = oldList;
|
|
2466
|
+
if (owned.length > 1) {
|
|
2467
|
+
log.task.warn("task-db v5: ambiguous remote list for merged project", {
|
|
2468
|
+
project: name,
|
|
2469
|
+
picked: oldList,
|
|
2470
|
+
candidates: owned.length
|
|
2471
|
+
});
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
insertProject.run({
|
|
2476
|
+
name,
|
|
2477
|
+
source,
|
|
2478
|
+
order_index: index,
|
|
2479
|
+
metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
|
|
2480
|
+
});
|
|
2481
|
+
const minority = selectMinority.all({ name, source });
|
|
2482
|
+
const minorityBySource = {};
|
|
2483
|
+
for (const row of minority) {
|
|
2484
|
+
normalizeMinority.run({ id: row.id, source, now: nowIso });
|
|
2485
|
+
const from = row.source ?? "local";
|
|
2486
|
+
minorityBySource[from] = (minorityBySource[from] ?? 0) + 1;
|
|
2487
|
+
}
|
|
2488
|
+
if (minority.length > 0) {
|
|
2489
|
+
log.task.warn("task-db v5: normalized minority-source tasks onto the project claim", {
|
|
2490
|
+
project: name,
|
|
2491
|
+
source,
|
|
2492
|
+
tasks: minority.length,
|
|
2493
|
+
from: minorityBySource
|
|
2494
|
+
});
|
|
2495
|
+
normalizedMinorityTotal += minority.length;
|
|
2496
|
+
}
|
|
2497
|
+
perProject.push({
|
|
2498
|
+
project: name,
|
|
2499
|
+
source,
|
|
2500
|
+
tasks: taskCount,
|
|
2501
|
+
from: groupsForProject.map((g) => `${g.category ?? ""}/${g.project ?? ""}`),
|
|
2502
|
+
remote_list: metadata.remote_list,
|
|
2503
|
+
...minority.length > 0 ? { normalized_minority: minority.length, normalized_from: minorityBySource } : {}
|
|
2504
|
+
});
|
|
2505
|
+
});
|
|
2506
|
+
const inboxCount = groups.filter((g) => !g.final).reduce((sum, g) => sum + g.taskCount, 0);
|
|
2507
|
+
const inboxProviderRows = handle.prepare(
|
|
2508
|
+
`SELECT id, source FROM tasks
|
|
2509
|
+
WHERE (project IS NULL OR project = '') AND source IS NOT NULL AND source != 'local'`
|
|
2510
|
+
).all();
|
|
2511
|
+
const inboxProviderBySource = {};
|
|
2512
|
+
if (inboxProviderRows.length > 0) {
|
|
2513
|
+
const toLocal = handle.prepare(
|
|
2514
|
+
`UPDATE tasks SET source = 'local', ext = NULL, sync_error = NULL, updated_at = @now,
|
|
2515
|
+
payload = CASE WHEN payload IS NOT NULL AND json_valid(payload)
|
|
2516
|
+
THEN json_remove(payload, '$.external_url') ELSE payload END
|
|
2517
|
+
WHERE id = @id`
|
|
2518
|
+
);
|
|
2519
|
+
for (const row of inboxProviderRows) {
|
|
2520
|
+
toLocal.run({ id: row.id, now: nowIso });
|
|
2521
|
+
inboxProviderBySource[row.source] = (inboxProviderBySource[row.source] ?? 0) + 1;
|
|
2522
|
+
}
|
|
2523
|
+
log.task.warn("task-db v5: reset provider-sourced Inbox tasks to local", {
|
|
2524
|
+
tasks: inboxProviderRows.length,
|
|
2525
|
+
from: inboxProviderBySource
|
|
2526
|
+
});
|
|
2527
|
+
}
|
|
2528
|
+
handle.exec(`DROP INDEX IF EXISTS tasks_category_project;`);
|
|
2529
|
+
handle.exec(`ALTER TABLE tasks DROP COLUMN category;`);
|
|
2530
|
+
handle.exec(`DROP TABLE IF EXISTS task_categories;`);
|
|
2531
|
+
return {
|
|
2532
|
+
perProject,
|
|
2533
|
+
inboxCount,
|
|
2534
|
+
sentinels: sentinels.length,
|
|
2535
|
+
normalizedMinority: normalizedMinorityTotal,
|
|
2536
|
+
inboxProviderReset: inboxProviderRows.length,
|
|
2537
|
+
inboxProviderResetFrom: inboxProviderBySource
|
|
2538
|
+
};
|
|
2539
|
+
})();
|
|
2540
|
+
log.task.info("task-db v5: category removed \u2014 project is now the only grouping layer", {
|
|
2541
|
+
projects: summary.perProject.length,
|
|
2542
|
+
inboxTasks: summary.inboxCount,
|
|
2543
|
+
sentinelsAbsorbed: summary.sentinels,
|
|
2544
|
+
normalizedMinoritySourceTasks: summary.normalizedMinority,
|
|
2545
|
+
inboxProviderTasksResetToLocal: summary.inboxProviderReset,
|
|
2546
|
+
inboxProviderResetFrom: summary.inboxProviderResetFrom,
|
|
2547
|
+
perProject: summary.perProject
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
function migrateReadMarkerToUnread(handle) {
|
|
2551
|
+
const summary = handle.transaction(() => {
|
|
2552
|
+
const carried = handle.prepare(
|
|
2553
|
+
`UPDATE tasks SET payload = json_set(payload, '$.unread', json('true'))
|
|
2554
|
+
WHERE payload IS NOT NULL AND json_valid(payload)
|
|
2555
|
+
AND json_extract(payload, '$.needs_attention') IN (1, 'true')
|
|
2556
|
+
AND json_extract(payload, '$.unread') IS NULL`
|
|
2557
|
+
).run().changes;
|
|
2558
|
+
const cleared = handle.prepare(
|
|
2559
|
+
`UPDATE tasks SET payload = json_remove(payload, '$.needs_attention')
|
|
2560
|
+
WHERE payload IS NOT NULL AND json_valid(payload)
|
|
2561
|
+
AND json_extract(payload, '$.needs_attention') IS NOT NULL`
|
|
2562
|
+
).run().changes;
|
|
2563
|
+
return { carried, cleared };
|
|
2564
|
+
})();
|
|
2565
|
+
if (summary.cleared > 0) {
|
|
2566
|
+
log.task.info("task-db v6: read marker renamed needs_attention \u2192 unread", summary);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
var TASK_DB_PATH, EXPLICIT_TASK_COLUMNS, JSON_COLUMNS, EXPLICIT_TASK_KEYS, RETIRED_TASK_KEYS, db, initAttempted, initError, TASK_COLUMNS, TASK_PROJECTS_DDL, SCHEMA_SQL, SCHEMA_VERSION, TASK_DB_PRE_V5_BACKUP_PATH;
|
|
2570
|
+
var init_task_db = __esm({
|
|
2571
|
+
"src/core/task-db.ts"() {
|
|
2572
|
+
"use strict";
|
|
2573
|
+
init_constants();
|
|
2574
|
+
init_logging();
|
|
2575
|
+
TASK_DB_PATH = path9.join(TASKS_DIR, "tasks.sqlite");
|
|
2576
|
+
EXPLICIT_TASK_COLUMNS = [
|
|
2577
|
+
"id",
|
|
2578
|
+
"title",
|
|
2579
|
+
"project",
|
|
2580
|
+
"status",
|
|
2581
|
+
"phase",
|
|
2582
|
+
"priority",
|
|
2583
|
+
"source",
|
|
2584
|
+
"parent_task_id",
|
|
2585
|
+
"due_date",
|
|
2586
|
+
"start_date",
|
|
2587
|
+
"created_at",
|
|
2588
|
+
"updated_at",
|
|
2589
|
+
"completed_at",
|
|
2590
|
+
"sprint",
|
|
2591
|
+
"focus_tier",
|
|
2592
|
+
"pinned",
|
|
2593
|
+
"ext",
|
|
2594
|
+
"tags",
|
|
2595
|
+
"depends_on",
|
|
2596
|
+
"session_ids",
|
|
2597
|
+
"note",
|
|
2598
|
+
"summary",
|
|
2599
|
+
"description",
|
|
2600
|
+
"conversation_log",
|
|
2601
|
+
"sync_error",
|
|
2602
|
+
"_synced_at"
|
|
2603
|
+
];
|
|
2604
|
+
JSON_COLUMNS = /* @__PURE__ */ new Set([
|
|
2605
|
+
"ext",
|
|
2606
|
+
"tags",
|
|
2607
|
+
"depends_on",
|
|
2608
|
+
"session_ids",
|
|
2609
|
+
"conversation_log"
|
|
2610
|
+
]);
|
|
2611
|
+
EXPLICIT_TASK_KEYS = /* @__PURE__ */ new Set([
|
|
2612
|
+
...EXPLICIT_TASK_COLUMNS,
|
|
2613
|
+
// Task type uses camelCase `_syncedAt`; column is snake `_synced_at`.
|
|
2614
|
+
"_syncedAt"
|
|
2615
|
+
]);
|
|
2616
|
+
RETIRED_TASK_KEYS = /* @__PURE__ */ new Set(["category", "needs_attention"]);
|
|
2617
|
+
db = null;
|
|
2618
|
+
initAttempted = false;
|
|
2619
|
+
initError = null;
|
|
2620
|
+
TASK_COLUMNS = EXPLICIT_TASK_COLUMNS;
|
|
2621
|
+
TASK_PROJECTS_DDL = `
|
|
2622
|
+
CREATE TABLE IF NOT EXISTS task_projects (
|
|
2623
|
+
name TEXT PRIMARY KEY COLLATE NOCASE,
|
|
2624
|
+
source TEXT NOT NULL DEFAULT 'local',
|
|
2625
|
+
order_index INTEGER,
|
|
2626
|
+
metadata TEXT
|
|
2627
|
+
);
|
|
2628
|
+
`;
|
|
2629
|
+
SCHEMA_SQL = `
|
|
2630
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
2631
|
+
id TEXT PRIMARY KEY,
|
|
2632
|
+
title TEXT NOT NULL,
|
|
2633
|
+
project TEXT,
|
|
2634
|
+
status TEXT,
|
|
2635
|
+
phase TEXT,
|
|
2636
|
+
priority TEXT,
|
|
2637
|
+
source TEXT,
|
|
2638
|
+
parent_task_id TEXT,
|
|
2639
|
+
due_date TEXT,
|
|
2640
|
+
start_date TEXT,
|
|
2641
|
+
created_at TEXT,
|
|
2642
|
+
updated_at TEXT,
|
|
2643
|
+
completed_at TEXT,
|
|
2644
|
+
sprint TEXT,
|
|
2645
|
+
focus_tier TEXT,
|
|
2646
|
+
pinned INTEGER DEFAULT 0,
|
|
2647
|
+
ext TEXT,
|
|
2648
|
+
tags TEXT,
|
|
2649
|
+
depends_on TEXT,
|
|
2650
|
+
session_ids TEXT,
|
|
2651
|
+
note TEXT,
|
|
2652
|
+
summary TEXT,
|
|
2653
|
+
description TEXT,
|
|
2654
|
+
conversation_log TEXT,
|
|
2655
|
+
sync_error TEXT,
|
|
2656
|
+
_synced_at TEXT,
|
|
2657
|
+
payload TEXT
|
|
2658
|
+
);
|
|
2659
|
+
CREATE INDEX IF NOT EXISTS tasks_project ON tasks(project);
|
|
2660
|
+
CREATE INDEX IF NOT EXISTS tasks_status ON tasks(status);
|
|
2661
|
+
CREATE INDEX IF NOT EXISTS tasks_source ON tasks(source);
|
|
2662
|
+
CREATE INDEX IF NOT EXISTS tasks_updated_at ON tasks(updated_at);
|
|
2663
|
+
CREATE INDEX IF NOT EXISTS tasks_parent ON tasks(parent_task_id);
|
|
2664
|
+
|
|
2665
|
+
-- Plugin ext-id indexes are no longer baked into SCHEMA_SQL \u2014 each plugin
|
|
2666
|
+
-- declares its own ext-index spec via PluginApi.registerExtIndex, and the
|
|
2667
|
+
-- loader calls ensureExtIndexes() after plugins finish loading. This keeps
|
|
2668
|
+
-- core agnostic to which plugins exist and lets external plugins (loaded
|
|
2669
|
+
-- from ~/.open-walnut/plugins/) bring their own indexes without touching
|
|
2670
|
+
-- core code.
|
|
2671
|
+
|
|
2672
|
+
${TASK_PROJECTS_DDL}
|
|
2673
|
+
|
|
2674
|
+
-- Virtual task-group name registry (local-only). Maps Task.group_id (stored in
|
|
2675
|
+
-- the tasks.payload blob) to a human-readable group label. Membership itself
|
|
2676
|
+
-- lives on the tasks (tasks.group_id); this table only holds the names.
|
|
2677
|
+
CREATE TABLE IF NOT EXISTS task_groups (
|
|
2678
|
+
id TEXT PRIMARY KEY,
|
|
2679
|
+
label TEXT NOT NULL,
|
|
2680
|
+
hidden INTEGER NOT NULL DEFAULT 0
|
|
2681
|
+
);
|
|
2682
|
+
|
|
2683
|
+
-- User-defined focus tiers (local-only). Ids are ct_* strings referenced by
|
|
2684
|
+
-- tasks.focus_tier; order_index preserves the user's Settings ordering.
|
|
2685
|
+
CREATE TABLE IF NOT EXISTS custom_tiers (
|
|
2686
|
+
id TEXT PRIMARY KEY,
|
|
2687
|
+
label TEXT NOT NULL,
|
|
2688
|
+
order_index INTEGER
|
|
2689
|
+
);
|
|
2690
|
+
`;
|
|
2691
|
+
SCHEMA_VERSION = 6;
|
|
2692
|
+
TASK_DB_PRE_V5_BACKUP_PATH = `${TASK_DB_PATH}.pre-v5.backup`;
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
// src/core/task-db-migration.ts
|
|
2697
|
+
import fs10 from "fs";
|
|
2698
|
+
import path10 from "path";
|
|
2699
|
+
import yaml3 from "js-yaml";
|
|
2700
|
+
async function runMigrationIfNeeded() {
|
|
2701
|
+
const db2 = getDb();
|
|
2702
|
+
if (!db2) {
|
|
2703
|
+
return { migrated: false, count: 0 };
|
|
2704
|
+
}
|
|
2705
|
+
const existing = db2.prepare("SELECT COUNT(*) AS n FROM tasks").get();
|
|
2706
|
+
if (existing.n > 0) {
|
|
2707
|
+
return { migrated: false, count: existing.n };
|
|
2708
|
+
}
|
|
2709
|
+
if (!fs10.existsSync(TASKS_FILE)) {
|
|
2710
|
+
return { migrated: false, count: 0 };
|
|
2711
|
+
}
|
|
2712
|
+
const store = await readJsonFile(TASKS_FILE, { version: 1, tasks: [] });
|
|
2713
|
+
const rawTasks = Array.isArray(store.tasks) ? store.tasks : [];
|
|
2714
|
+
const legacyCategories = store.categories ?? {};
|
|
2715
|
+
const tasks = rawTasks.filter(
|
|
2716
|
+
(t) => t?.title !== ".metadata_project" && t?.title !== ".metadata_category"
|
|
2717
|
+
);
|
|
2718
|
+
const sentinels = rawTasks.filter(
|
|
2719
|
+
(t) => t?.title === ".metadata_project" || t?.title === ".metadata_category"
|
|
2720
|
+
);
|
|
2721
|
+
const targetOf = (t) => promoteLegacyGroup(t.category ?? "", t.project ?? "");
|
|
2722
|
+
const spellingCounts = /* @__PURE__ */ new Map();
|
|
2723
|
+
for (const t of tasks) {
|
|
2724
|
+
const target = targetOf(t);
|
|
2725
|
+
if (!target) continue;
|
|
2726
|
+
const lower = target.toLowerCase();
|
|
2727
|
+
const bucket = spellingCounts.get(lower) ?? /* @__PURE__ */ new Map();
|
|
2728
|
+
bucket.set(target, (bucket.get(target) ?? 0) + 1);
|
|
2729
|
+
spellingCounts.set(lower, bucket);
|
|
2730
|
+
}
|
|
2731
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const [lower, bucket] of spellingCounts) {
|
|
2733
|
+
canonical.set(
|
|
2734
|
+
lower,
|
|
2735
|
+
[...bucket.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))[0][0]
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
const finalProjectOf = (t) => {
|
|
2739
|
+
const target = targetOf(t);
|
|
2740
|
+
return target ? canonical.get(target.toLowerCase()) ?? target : "";
|
|
2741
|
+
};
|
|
2742
|
+
const projectRows = /* @__PURE__ */ new Map();
|
|
2743
|
+
const categorySource = (category) => {
|
|
2744
|
+
const key = Object.keys(legacyCategories).find((k) => k.toLowerCase() === category.trim().toLowerCase());
|
|
2745
|
+
return key ? String(legacyCategories[key]?.source ?? "local") : "local";
|
|
2746
|
+
};
|
|
2747
|
+
for (const t of tasks) {
|
|
2748
|
+
const name = finalProjectOf(t);
|
|
2749
|
+
if (!name) continue;
|
|
2750
|
+
const row = projectRows.get(name.toLowerCase()) ?? { name, source: "local", metadata: {}, weights: /* @__PURE__ */ new Map(), ownedGroups: /* @__PURE__ */ new Map() };
|
|
2751
|
+
const legacyCat = (t.category ?? "").trim();
|
|
2752
|
+
const src = categorySource(legacyCat);
|
|
2753
|
+
if (src !== "local") {
|
|
2754
|
+
row.weights.set(src, (row.weights.get(src) ?? 0) + 1);
|
|
2755
|
+
const legacyProj = (t.project ?? "").trim();
|
|
2756
|
+
const groupKey = `${legacyCat.toLowerCase()}/${legacyProj.toLowerCase()}`;
|
|
2757
|
+
const group = row.ownedGroups.get(groupKey) ?? { category: legacyCat, project: legacyProj, count: 0 };
|
|
2758
|
+
group.count += 1;
|
|
2759
|
+
row.ownedGroups.set(groupKey, group);
|
|
2760
|
+
}
|
|
2761
|
+
if (legacyCat) {
|
|
2762
|
+
const existing2 = row.metadata.legacy_category;
|
|
2763
|
+
if (existing2 === void 0) row.metadata.legacy_category = legacyCat;
|
|
2764
|
+
else if (Array.isArray(existing2)) {
|
|
2765
|
+
if (!existing2.includes(legacyCat)) existing2.push(legacyCat);
|
|
2766
|
+
} else if (existing2 !== legacyCat) {
|
|
2767
|
+
row.metadata.legacy_category = [existing2, legacyCat].sort();
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
projectRows.set(name.toLowerCase(), row);
|
|
2771
|
+
}
|
|
2772
|
+
for (const row of projectRows.values()) {
|
|
2773
|
+
row.source = pickMajoritySource(row.weights, row.name);
|
|
2774
|
+
if (row.source !== "ms-todo") continue;
|
|
2775
|
+
const owned = [...row.ownedGroups.values()].filter((g) => categorySource(g.category) === "ms-todo").sort((a, b) => b.count - a.count || (a.project < b.project ? -1 : 1));
|
|
2776
|
+
const pick = owned[0];
|
|
2777
|
+
if (!pick) continue;
|
|
2778
|
+
const oldList = legacyListName(pick.category, pick.project);
|
|
2779
|
+
if (oldList && oldList.toLowerCase() !== row.name.toLowerCase()) {
|
|
2780
|
+
row.metadata.remote_list = oldList;
|
|
2781
|
+
}
|
|
2782
|
+
if (owned.length > 1) {
|
|
2783
|
+
log.task.warn("task-db migration: ambiguous remote list for merged project", {
|
|
2784
|
+
project: row.name,
|
|
2785
|
+
picked: oldList,
|
|
2786
|
+
candidates: owned.length
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
for (const sentinel of sentinels) {
|
|
2791
|
+
const name = finalProjectOf(sentinel);
|
|
2792
|
+
if (!name) continue;
|
|
2793
|
+
const row = projectRows.get(name.toLowerCase());
|
|
2794
|
+
if (!row) continue;
|
|
2795
|
+
let settings;
|
|
2796
|
+
try {
|
|
2797
|
+
settings = yaml3.load(sentinel.description ?? "");
|
|
2798
|
+
} catch {
|
|
2799
|
+
settings = null;
|
|
2800
|
+
}
|
|
2801
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
|
|
2802
|
+
const overwrite = sentinel.title === ".metadata_project";
|
|
2803
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
2804
|
+
if (overwrite || !(key in row.metadata)) row.metadata[key] = value;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
const taskInsertCols = [...TASK_COLUMNS, "payload"];
|
|
2808
|
+
const taskInsertSql = "INSERT INTO tasks (" + taskInsertCols.join(", ") + ") VALUES (" + taskInsertCols.map((c) => "@" + c).join(", ") + ")";
|
|
2809
|
+
const projectInsertSql = "INSERT INTO task_projects (name, source, order_index, metadata) VALUES (@name, @source, @order_index, @metadata) ON CONFLICT(name) DO NOTHING";
|
|
2810
|
+
const sourceNormalized = [];
|
|
2811
|
+
transaction((h) => {
|
|
2812
|
+
const insertTask = h.prepare(taskInsertSql);
|
|
2813
|
+
const insertProject = h.prepare(projectInsertSql);
|
|
2814
|
+
[...projectRows.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).forEach((row, index) => {
|
|
2815
|
+
insertProject.run({
|
|
2816
|
+
name: row.name,
|
|
2817
|
+
source: row.source,
|
|
2818
|
+
order_index: index,
|
|
2819
|
+
metadata: Object.keys(row.metadata).length > 0 ? JSON.stringify(row.metadata) : null
|
|
2820
|
+
});
|
|
2821
|
+
});
|
|
2822
|
+
for (const task of tasks) {
|
|
2823
|
+
if (!task || typeof task !== "object" || typeof task.id !== "string") {
|
|
2824
|
+
log.task.warn("task-db migration: skipping malformed task", {
|
|
2825
|
+
sample: String(task?.id ?? "<no id>")
|
|
2826
|
+
});
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
if (task.title?.trim().startsWith(".metadata")) continue;
|
|
2830
|
+
const { category: _legacyCategory, ...rest } = task;
|
|
2831
|
+
const project = finalProjectOf(task);
|
|
2832
|
+
const registryRow = project ? projectRows.get(project.toLowerCase()) : void 0;
|
|
2833
|
+
const authoritativeSource = project ? registryRow?.source ?? task.source : "local";
|
|
2834
|
+
const normalized = {};
|
|
2835
|
+
if (task.source !== authoritativeSource) {
|
|
2836
|
+
normalized.source = authoritativeSource;
|
|
2837
|
+
normalized.ext = void 0;
|
|
2838
|
+
normalized.external_url = void 0;
|
|
2839
|
+
normalized.sync_error = void 0;
|
|
2840
|
+
sourceNormalized.push({ id: task.id, from: task.source, to: authoritativeSource, project });
|
|
2841
|
+
}
|
|
2842
|
+
const partial = taskToRow({ ...rest, project, ...normalized });
|
|
2843
|
+
if (normalized.source !== void 0) partial.ext = null;
|
|
2844
|
+
const bound = {};
|
|
2845
|
+
for (const col of taskInsertCols) {
|
|
2846
|
+
bound[col] = partial[col] === void 0 ? null : partial[col];
|
|
2847
|
+
}
|
|
2848
|
+
try {
|
|
2849
|
+
insertTask.run(bound);
|
|
2850
|
+
} catch (err) {
|
|
2851
|
+
log.task.warn("task-db migration: failed to insert task, skipping", {
|
|
2852
|
+
id: task.id,
|
|
2853
|
+
err: String(err)
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
if (sourceNormalized.length > 0) {
|
|
2859
|
+
log.task.warn("task-db migration: normalized task sources onto the project claim", {
|
|
2860
|
+
tasks: sourceNormalized.length,
|
|
2861
|
+
sample: sourceNormalized.slice(0, 20)
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
const after = db2.prepare("SELECT COUNT(*) AS n FROM tasks").get();
|
|
2865
|
+
const count = after.n;
|
|
2866
|
+
const backupPath = path10.join(path10.dirname(TASKS_FILE), "tasks.json.migrated-from-json.backup");
|
|
2867
|
+
try {
|
|
2868
|
+
fs10.copyFileSync(TASKS_FILE, backupPath);
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
log.task.warn("task-db migration: backup copy failed (non-fatal)", {
|
|
2871
|
+
path: backupPath,
|
|
2872
|
+
err: String(err)
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
log.task.info("task-db: migrated tasks from tasks.json", {
|
|
2876
|
+
count,
|
|
2877
|
+
projectCount: projectRows.size
|
|
2878
|
+
});
|
|
2879
|
+
return { migrated: true, count };
|
|
2880
|
+
}
|
|
2881
|
+
var init_task_db_migration = __esm({
|
|
2882
|
+
"src/core/task-db-migration.ts"() {
|
|
2883
|
+
"use strict";
|
|
2884
|
+
init_constants();
|
|
2885
|
+
init_logging();
|
|
2886
|
+
init_fs();
|
|
2887
|
+
init_task_db();
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
|
|
2891
|
+
// src/core/memory-dir-migration.ts
|
|
2892
|
+
import fs11 from "fs";
|
|
2893
|
+
import fsp6 from "fs/promises";
|
|
2894
|
+
import path11 from "path";
|
|
2895
|
+
function bodyOf(content) {
|
|
2896
|
+
const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
|
|
2897
|
+
return fm ? content.slice(fm[0].length) : content;
|
|
2898
|
+
}
|
|
2899
|
+
function mergeMemoryContents(base, legacy, legacyLabel) {
|
|
2900
|
+
const legacyBody = bodyOf(legacy).trim();
|
|
2901
|
+
if (!legacyBody) return base;
|
|
2902
|
+
const head = base.endsWith("\n") ? base : `${base}
|
|
2903
|
+
`;
|
|
2904
|
+
return `${head}
|
|
2905
|
+
## Merged from ${legacyLabel} (legacy)
|
|
2906
|
+
|
|
2907
|
+
${legacyBody}
|
|
2908
|
+
`;
|
|
2909
|
+
}
|
|
2910
|
+
function mtimeMs(file) {
|
|
2911
|
+
try {
|
|
2912
|
+
return fs11.statSync(file).mtimeMs;
|
|
2913
|
+
} catch {
|
|
2914
|
+
return 0;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
function findCandidates(root) {
|
|
2918
|
+
const out = [];
|
|
2919
|
+
let level1;
|
|
2920
|
+
try {
|
|
2921
|
+
level1 = fs11.readdirSync(root, { withFileTypes: true });
|
|
2922
|
+
} catch {
|
|
2923
|
+
return out;
|
|
2924
|
+
}
|
|
2925
|
+
for (const cat of level1) {
|
|
2926
|
+
if (!cat.isDirectory()) continue;
|
|
2927
|
+
const catDir = path11.join(root, cat.name);
|
|
2928
|
+
if (fs11.existsSync(path11.join(catDir, MEMORY_FILE_NAME))) continue;
|
|
2929
|
+
let level2;
|
|
2930
|
+
try {
|
|
2931
|
+
level2 = fs11.readdirSync(catDir, { withFileTypes: true });
|
|
2932
|
+
} catch {
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
for (const proj of level2) {
|
|
2936
|
+
if (!proj.isDirectory()) continue;
|
|
2937
|
+
const sourceDir = path11.join(catDir, proj.name);
|
|
2938
|
+
if (!fs11.existsSync(path11.join(sourceDir, MEMORY_FILE_NAME))) continue;
|
|
2939
|
+
out.push({ category: cat.name, project: proj.name, sourceDir });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return out;
|
|
2943
|
+
}
|
|
2944
|
+
async function drainDir(from, to) {
|
|
2945
|
+
let entries;
|
|
2946
|
+
try {
|
|
2947
|
+
entries = await fsp6.readdir(from);
|
|
2948
|
+
} catch {
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
for (const name of entries) {
|
|
2952
|
+
const target = path11.join(to, name);
|
|
2953
|
+
if (fs11.existsSync(target)) continue;
|
|
2954
|
+
await fsp6.rename(path11.join(from, name), target).catch((err) => {
|
|
2955
|
+
log.memory.warn("memory-dir migration: entry move failed", {
|
|
2956
|
+
from: path11.join(from, name),
|
|
2957
|
+
to: target,
|
|
2958
|
+
error: String(err)
|
|
2959
|
+
});
|
|
2960
|
+
});
|
|
2961
|
+
}
|
|
2962
|
+
await fsp6.rmdir(from).catch(() => {
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
async function migrateProjectMemoryDirs() {
|
|
2966
|
+
const result = { ran: false, moved: 0, merged: 0 };
|
|
2967
|
+
try {
|
|
2968
|
+
if (!fs11.existsSync(PROJECTS_MEMORY_DIR)) return result;
|
|
2969
|
+
if (fs11.existsSync(MEMORY_DIR_MIGRATION_MARKER)) return result;
|
|
2970
|
+
result.ran = true;
|
|
2971
|
+
const candidates = findCandidates(PROJECTS_MEMORY_DIR);
|
|
2972
|
+
const touchedCategories = new Set(candidates.map((c) => c.category));
|
|
2973
|
+
for (const c of candidates) {
|
|
2974
|
+
const targetDir = path11.join(PROJECTS_MEMORY_DIR, c.project);
|
|
2975
|
+
const label = `${c.category}/${c.project}`;
|
|
2976
|
+
try {
|
|
2977
|
+
if (!fs11.existsSync(targetDir)) {
|
|
2978
|
+
await fsp6.rename(c.sourceDir, targetDir);
|
|
2979
|
+
result.moved += 1;
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
const sourceMem = path11.join(c.sourceDir, MEMORY_FILE_NAME);
|
|
2983
|
+
const targetMem = path11.join(targetDir, MEMORY_FILE_NAME);
|
|
2984
|
+
if (!fs11.existsSync(targetMem)) {
|
|
2985
|
+
await fsp6.rename(sourceMem, targetMem);
|
|
2986
|
+
await drainDir(c.sourceDir, targetDir);
|
|
2987
|
+
result.moved += 1;
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
const sourceContent = await fsp6.readFile(sourceMem, "utf-8");
|
|
2991
|
+
const targetContent = await fsp6.readFile(targetMem, "utf-8");
|
|
2992
|
+
const sourceNewer = mtimeMs(sourceMem) > mtimeMs(targetMem);
|
|
2993
|
+
const mergedContent = sourceNewer ? mergeMemoryContents(sourceContent, targetContent, c.project) : mergeMemoryContents(targetContent, sourceContent, label);
|
|
2994
|
+
await fsp6.writeFile(targetMem, mergedContent, "utf-8");
|
|
2995
|
+
await fsp6.rm(sourceMem, { force: true });
|
|
2996
|
+
await drainDir(c.sourceDir, targetDir);
|
|
2997
|
+
result.merged += 1;
|
|
2998
|
+
} catch (err) {
|
|
2999
|
+
log.memory.warn("memory-dir migration: candidate failed (skipped)", {
|
|
3000
|
+
candidate: label,
|
|
3001
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
for (const cat of touchedCategories) {
|
|
3006
|
+
await fsp6.rmdir(path11.join(PROJECTS_MEMORY_DIR, cat)).catch(() => {
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
await fsp6.writeFile(
|
|
3010
|
+
MEMORY_DIR_MIGRATION_MARKER,
|
|
3011
|
+
`migrated ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3012
|
+
`,
|
|
3013
|
+
"utf-8"
|
|
3014
|
+
);
|
|
3015
|
+
if (result.moved > 0 || result.merged > 0) {
|
|
3016
|
+
log.memory.info("memory-dir migration: flattened project memory to one level", {
|
|
3017
|
+
moved: result.moved,
|
|
3018
|
+
merged: result.merged
|
|
3019
|
+
});
|
|
3020
|
+
}
|
|
3021
|
+
} catch (err) {
|
|
3022
|
+
log.memory.warn("memory-dir migration failed (will retry next start)", {
|
|
3023
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
return result;
|
|
3027
|
+
}
|
|
3028
|
+
var MEMORY_DIR_MIGRATION_MARKER, MEMORY_FILE_NAME;
|
|
3029
|
+
var init_memory_dir_migration = __esm({
|
|
3030
|
+
"src/core/memory-dir-migration.ts"() {
|
|
3031
|
+
"use strict";
|
|
3032
|
+
init_constants();
|
|
3033
|
+
init_logging();
|
|
3034
|
+
MEMORY_DIR_MIGRATION_MARKER = path11.join(PROJECTS_MEMORY_DIR, ".migrated-project-only");
|
|
3035
|
+
MEMORY_FILE_NAME = "MEMORY.md";
|
|
3036
|
+
}
|
|
3037
|
+
});
|
|
3038
|
+
|
|
3039
|
+
// src/core/ext-index-registry.ts
|
|
3040
|
+
function getExtIndexSpec(source) {
|
|
3041
|
+
return specs.get(source);
|
|
3042
|
+
}
|
|
3043
|
+
var specs;
|
|
3044
|
+
var init_ext_index_registry = __esm({
|
|
3045
|
+
"src/core/ext-index-registry.ts"() {
|
|
3046
|
+
"use strict";
|
|
3047
|
+
specs = /* @__PURE__ */ new Map();
|
|
3048
|
+
}
|
|
3049
|
+
});
|
|
3050
|
+
|
|
3051
|
+
// src/core/task-manager.ts
|
|
3052
|
+
import fsSync from "fs";
|
|
3053
|
+
function invalidateTaskStoreCache() {
|
|
3054
|
+
taskStoreCache = null;
|
|
3055
|
+
taskStoreCacheDataVersion = null;
|
|
3056
|
+
}
|
|
3057
|
+
function withWriteLock2(fn) {
|
|
3058
|
+
const prev = writeLock2;
|
|
3059
|
+
let resolve;
|
|
3060
|
+
writeLock2 = new Promise((r) => {
|
|
3061
|
+
resolve = r;
|
|
3062
|
+
});
|
|
3063
|
+
return prev.then(() => withFileLock(TASKS_FILE, async () => {
|
|
3064
|
+
writeLockDepth += 1;
|
|
3065
|
+
try {
|
|
3066
|
+
return await fn();
|
|
3067
|
+
} finally {
|
|
3068
|
+
writeLockDepth -= 1;
|
|
3069
|
+
}
|
|
3070
|
+
})).finally(() => {
|
|
3071
|
+
invalidateTaskStoreCache();
|
|
3072
|
+
resolve();
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
function withWriteLockIfFree(fn) {
|
|
3076
|
+
return writeLockDepth > 0 ? fn() : withWriteLock2(fn);
|
|
3077
|
+
}
|
|
3078
|
+
async function ensureInit() {
|
|
3079
|
+
if (!initialized) {
|
|
3080
|
+
await initDirectories();
|
|
3081
|
+
getDb();
|
|
3082
|
+
await runMigrationIfNeeded();
|
|
3083
|
+
await migrateProjectMemoryDirs();
|
|
3084
|
+
await seedProjectsFromConfig();
|
|
3085
|
+
initialized = true;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
async function seedProjectsFromConfig() {
|
|
3089
|
+
const config = await getConfig();
|
|
3090
|
+
return withWriteLockIfFree(async () => {
|
|
3091
|
+
const db2 = getDb();
|
|
3092
|
+
const desired = [];
|
|
3093
|
+
let added = 0;
|
|
3094
|
+
transaction((handle) => {
|
|
3095
|
+
const existingRows = handle.prepare("SELECT name FROM task_projects").all();
|
|
3096
|
+
const existing = new Set(existingRows.map((r) => r.name.trim().toLowerCase()));
|
|
3097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3098
|
+
const addIfNew = (rawName, source) => {
|
|
3099
|
+
const name = (rawName ?? "").trim();
|
|
3100
|
+
if (!name) return;
|
|
3101
|
+
try {
|
|
3102
|
+
assertValidProjectName(name);
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
log.task.warn("seedProjectsFromConfig: skipping invalid project name", {
|
|
3105
|
+
name,
|
|
3106
|
+
source,
|
|
3107
|
+
err: String(err)
|
|
3108
|
+
});
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
const key = name.toLowerCase();
|
|
3112
|
+
if (seen.has(key) || existing.has(key)) return;
|
|
3113
|
+
seen.add(key);
|
|
3114
|
+
desired.push({ name, source });
|
|
3115
|
+
};
|
|
3116
|
+
const plugins = config.plugins ?? {};
|
|
3117
|
+
for (const [pluginId, cfg] of Object.entries(plugins)) {
|
|
3118
|
+
const reserved = cfg.project;
|
|
3119
|
+
if (typeof reserved === "string" && reserved) addIfNew(reserved, pluginId);
|
|
3120
|
+
}
|
|
3121
|
+
const taskRows = handle.prepare("SELECT project, source, COUNT(*) AS n FROM tasks WHERE project IS NOT NULL AND project != '' GROUP BY project, source ORDER BY n DESC").all();
|
|
3122
|
+
for (const row of taskRows) {
|
|
3123
|
+
addIfNew(row.project, row.source ?? "local");
|
|
3124
|
+
}
|
|
3125
|
+
if (desired.length === 0) return;
|
|
3126
|
+
const nextOrder = handle.prepare("SELECT COALESCE(MAX(order_index), -1) + 1 AS next FROM task_projects").get().next;
|
|
3127
|
+
const stmt = handle.prepare(
|
|
3128
|
+
"INSERT INTO task_projects (name, source, order_index) VALUES (?, ?, ?) ON CONFLICT(name) DO NOTHING"
|
|
3129
|
+
);
|
|
3130
|
+
let idx = nextOrder;
|
|
3131
|
+
for (const { name, source } of desired) {
|
|
3132
|
+
stmt.run(name, source, idx);
|
|
3133
|
+
idx += 1;
|
|
3134
|
+
added += 1;
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
void db2;
|
|
3138
|
+
if (added === 0) return;
|
|
3139
|
+
invalidateTaskStoreCache();
|
|
3140
|
+
log.task.info("seeded task_projects", { added });
|
|
3141
|
+
});
|
|
3142
|
+
}
|
|
3143
|
+
function assertValidProjectName(name) {
|
|
3144
|
+
const trimmed = (name ?? "").trim();
|
|
3145
|
+
if (!trimmed) {
|
|
3146
|
+
throw new InvalidProjectNameError(
|
|
3147
|
+
"Project name must be a non-empty string. To clear a project, move its tasks to Inbox.",
|
|
3148
|
+
trimmed
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
if (trimmed.includes("/") || trimmed.includes("\\")) {
|
|
3152
|
+
throw new InvalidProjectNameError(
|
|
3153
|
+
`Invalid project name "${name}": path separators ('/', '\\') are not allowed \u2014 a project name becomes a directory segment.`,
|
|
3154
|
+
trimmed
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
if (trimmed.includes("..")) {
|
|
3158
|
+
throw new InvalidProjectNameError(
|
|
3159
|
+
`Invalid project name "${name}": '..' is not allowed \u2014 a project name becomes a directory segment.`,
|
|
3160
|
+
trimmed
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
3163
|
+
if (trimmed.includes("\0")) {
|
|
3164
|
+
throw new InvalidProjectNameError(
|
|
3165
|
+
`Invalid project name "${name}": it contains a NUL character.`,
|
|
3166
|
+
trimmed
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
if (trimmed.startsWith(".")) {
|
|
3170
|
+
throw new InvalidProjectNameError(
|
|
3171
|
+
`Invalid project name "${name}": it cannot start with '.' (hidden directory / retired .metadata namespace).`,
|
|
3172
|
+
trimmed
|
|
3173
|
+
);
|
|
3174
|
+
}
|
|
3175
|
+
return trimmed;
|
|
3176
|
+
}
|
|
3177
|
+
function getFindByExtIdStmt(source, jsonPath) {
|
|
3178
|
+
const cacheKey = `${source}|${jsonPath}`;
|
|
3179
|
+
const cached = findByExtIdStmts.get(cacheKey);
|
|
3180
|
+
if (cached) return cached;
|
|
3181
|
+
const db2 = getDb();
|
|
3182
|
+
const sourceLiteral = source.replace(/'/g, "''");
|
|
3183
|
+
const pathLiteral = jsonPath.replace(/'/g, "''");
|
|
3184
|
+
const sql = `SELECT * FROM tasks WHERE source = '${sourceLiteral}' AND json_extract(ext, '${pathLiteral}') = ? LIMIT 1`;
|
|
3185
|
+
const stmt = db2.prepare(sql);
|
|
3186
|
+
findByExtIdStmts.set(cacheKey, stmt);
|
|
3187
|
+
return stmt;
|
|
3188
|
+
}
|
|
3189
|
+
async function findTaskByExtId(source, extId) {
|
|
3190
|
+
if (!extId) return void 0;
|
|
3191
|
+
await ensureInit();
|
|
3192
|
+
const spec = getExtIndexSpec(source);
|
|
3193
|
+
if (!spec) return void 0;
|
|
3194
|
+
for (const p of spec.paths) {
|
|
3195
|
+
const row = getFindByExtIdStmt(source, p.json).get(extId);
|
|
3196
|
+
if (row) return rowToTask(row);
|
|
3197
|
+
}
|
|
3198
|
+
return void 0;
|
|
3199
|
+
}
|
|
3200
|
+
function pluginPrimaryExtPath(source) {
|
|
3201
|
+
const spec = getExtIndexSpec(source);
|
|
3202
|
+
if (!spec || spec.paths.length === 0) {
|
|
3203
|
+
throw new Error(
|
|
3204
|
+
`listUnsyncedTasks/listSyncErrorTasks: no ext-index registered for source "${source}". The plugin must call PluginApi.registerExtIndex during load.`
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
return spec.paths[0].json;
|
|
3208
|
+
}
|
|
3209
|
+
async function listUnsyncedTasks(pluginId) {
|
|
3210
|
+
await ensureInit();
|
|
3211
|
+
const extPath = pluginPrimaryExtPath(pluginId);
|
|
3212
|
+
const db2 = getDb();
|
|
3213
|
+
const pathLiteral = extPath.replace(/'/g, "''");
|
|
3214
|
+
const sql = `SELECT * FROM tasks WHERE source = ? AND status != 'done'
|
|
3215
|
+
AND (ext IS NULL OR json_extract(ext, '${pathLiteral}') IS NULL)`;
|
|
3216
|
+
const rows = db2.prepare(sql).all(pluginId);
|
|
3217
|
+
return rows.map(rowToTask);
|
|
3218
|
+
}
|
|
3219
|
+
var initialized, STORE_CACHE_ENABLED, taskStoreCache, taskStoreCacheDataVersion, writeLock2, writeLockDepth, VALID_PRIORITIES_SET, InvalidProjectNameError, findByExtIdStmts;
|
|
3220
|
+
var init_task_manager = __esm({
|
|
3221
|
+
"src/core/task-manager.ts"() {
|
|
3222
|
+
"use strict";
|
|
3223
|
+
init_constants();
|
|
3224
|
+
init_file_lock();
|
|
3225
|
+
init_logging();
|
|
3226
|
+
init_format();
|
|
3227
|
+
init_init();
|
|
3228
|
+
init_config_manager();
|
|
3229
|
+
init_event_bus();
|
|
3230
|
+
init_types();
|
|
3231
|
+
init_phase();
|
|
3232
|
+
init_integration_registry();
|
|
3233
|
+
init_task_db();
|
|
3234
|
+
init_task_db_migration();
|
|
3235
|
+
init_memory_dir_migration();
|
|
3236
|
+
init_ext_index_registry();
|
|
3237
|
+
initialized = false;
|
|
3238
|
+
STORE_CACHE_ENABLED = process.env.WALNUT_STORE_CACHE !== "0";
|
|
3239
|
+
taskStoreCache = null;
|
|
3240
|
+
taskStoreCacheDataVersion = null;
|
|
3241
|
+
writeLock2 = Promise.resolve();
|
|
3242
|
+
writeLockDepth = 0;
|
|
3243
|
+
VALID_PRIORITIES_SET = new Set(VALID_PRIORITIES);
|
|
3244
|
+
InvalidProjectNameError = class extends Error {
|
|
3245
|
+
project;
|
|
3246
|
+
constructor(message, project) {
|
|
3247
|
+
super(message);
|
|
3248
|
+
this.name = "InvalidProjectNameError";
|
|
3249
|
+
this.project = project;
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
findByExtIdStmts = /* @__PURE__ */ new Map();
|
|
3253
|
+
}
|
|
3254
|
+
});
|
|
3255
|
+
|
|
3256
|
+
// src/integrations/jira/jira-client.ts
|
|
3257
|
+
import https from "https";
|
|
3258
|
+
import http from "http";
|
|
3259
|
+
import { URL } from "url";
|
|
3260
|
+
var MAX_RETRIES, INITIAL_BACKOFF_MS, JiraClient;
|
|
3261
|
+
var init_jira_client = __esm({
|
|
3262
|
+
"src/integrations/jira/jira-client.ts"() {
|
|
3263
|
+
"use strict";
|
|
3264
|
+
init_logging();
|
|
3265
|
+
MAX_RETRIES = 3;
|
|
3266
|
+
INITIAL_BACKOFF_MS = 1e3;
|
|
3267
|
+
JiraClient = class {
|
|
3268
|
+
baseUrl;
|
|
3269
|
+
authHeader;
|
|
3270
|
+
logger = createSubsystemLogger("jira");
|
|
3271
|
+
constructor(config) {
|
|
3272
|
+
this.baseUrl = config.base_url.replace(/\/+$/, "");
|
|
3273
|
+
if (config.auth.type === "api-token") {
|
|
3274
|
+
if (!config.auth.email) throw new Error("Jira API token auth requires email");
|
|
3275
|
+
const credentials = Buffer.from(`${config.auth.email}:${config.auth.token}`).toString("base64");
|
|
3276
|
+
this.authHeader = `Basic ${credentials}`;
|
|
3277
|
+
} else {
|
|
3278
|
+
this.authHeader = `Bearer ${config.auth.token}`;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
// ── Issue CRUD ──
|
|
3282
|
+
async getIssue(issueIdOrKey) {
|
|
3283
|
+
return this.request(
|
|
3284
|
+
"GET",
|
|
3285
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
async searchIssues(jql, opts) {
|
|
3289
|
+
const params = new URLSearchParams({
|
|
3290
|
+
jql,
|
|
3291
|
+
maxResults: String(opts?.maxResults ?? 100),
|
|
3292
|
+
startAt: String(opts?.startAt ?? 0)
|
|
3293
|
+
});
|
|
3294
|
+
if (opts?.fields) params.set("fields", opts.fields.join(","));
|
|
3295
|
+
return this.request("GET", `/rest/api/3/search?${params}`);
|
|
3296
|
+
}
|
|
3297
|
+
async createIssue(input) {
|
|
3298
|
+
return this.request("POST", "/rest/api/3/issue", input);
|
|
3299
|
+
}
|
|
3300
|
+
async updateIssue(issueIdOrKey, input) {
|
|
3301
|
+
await this.request(
|
|
3302
|
+
"PUT",
|
|
3303
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}`,
|
|
3304
|
+
input
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
// ── Comments ──
|
|
3308
|
+
async addComment(issueIdOrKey, body) {
|
|
3309
|
+
return this.request(
|
|
3310
|
+
"POST",
|
|
3311
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/comment`,
|
|
3312
|
+
{ body }
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
async updateComment(issueIdOrKey, commentId, body) {
|
|
3316
|
+
return this.request(
|
|
3317
|
+
"PUT",
|
|
3318
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/comment/${commentId}`,
|
|
3319
|
+
{ body }
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
// ── Transitions (workflow) ──
|
|
3323
|
+
async getTransitions(issueIdOrKey) {
|
|
3324
|
+
return this.request(
|
|
3325
|
+
"GET",
|
|
3326
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/transitions`
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
async doTransition(issueIdOrKey, transitionId) {
|
|
3330
|
+
await this.request(
|
|
3331
|
+
"POST",
|
|
3332
|
+
`/rest/api/3/issue/${encodeURIComponent(issueIdOrKey)}/transitions`,
|
|
3333
|
+
{ transition: { id: transitionId } }
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
// ── Discovery ──
|
|
3337
|
+
async getMyself() {
|
|
3338
|
+
return this.request("GET", "/rest/api/3/myself");
|
|
3339
|
+
}
|
|
3340
|
+
// ── HTTP transport ──
|
|
3341
|
+
async request(method, path13, body) {
|
|
3342
|
+
let lastError = null;
|
|
3343
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
3344
|
+
try {
|
|
3345
|
+
return await this.doRequest(method, path13, body);
|
|
3346
|
+
} catch (err) {
|
|
3347
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
3348
|
+
const status = err.statusCode;
|
|
3349
|
+
if (status !== 429 && (!status || status < 500)) {
|
|
3350
|
+
throw lastError;
|
|
3351
|
+
}
|
|
3352
|
+
if (attempt < MAX_RETRIES) {
|
|
3353
|
+
const backoff = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
|
|
3354
|
+
this.logger.debug("Jira API retrying", { method, path: path13, attempt: attempt + 1, backoff });
|
|
3355
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
throw lastError ?? new Error("Jira request failed");
|
|
3360
|
+
}
|
|
3361
|
+
doRequest(method, path13, body) {
|
|
3362
|
+
return new Promise((resolve, reject) => {
|
|
3363
|
+
const url = new URL(path13, this.baseUrl);
|
|
3364
|
+
const isHttps = url.protocol === "https:";
|
|
3365
|
+
const transport = isHttps ? https : http;
|
|
3366
|
+
const bodyStr = body ? JSON.stringify(body) : void 0;
|
|
3367
|
+
const options = {
|
|
3368
|
+
hostname: url.hostname,
|
|
3369
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
3370
|
+
path: url.pathname + url.search,
|
|
3371
|
+
method,
|
|
3372
|
+
headers: {
|
|
3373
|
+
"Authorization": this.authHeader,
|
|
3374
|
+
"Accept": "application/json",
|
|
3375
|
+
...bodyStr ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(bodyStr) } : {}
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
const req = transport.request(options, (res) => {
|
|
3379
|
+
const chunks = [];
|
|
3380
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
3381
|
+
res.on("end", () => {
|
|
3382
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3383
|
+
const status = res.statusCode ?? 0;
|
|
3384
|
+
if (status >= 200 && status < 300) {
|
|
3385
|
+
if (!raw.trim()) {
|
|
3386
|
+
resolve(void 0);
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
try {
|
|
3390
|
+
resolve(JSON.parse(raw));
|
|
3391
|
+
} catch {
|
|
3392
|
+
resolve(raw);
|
|
3393
|
+
}
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
const err = new Error(`Jira API ${method} ${path13} \u2192 ${status}: ${raw.slice(0, 500)}`);
|
|
3397
|
+
err.statusCode = status;
|
|
3398
|
+
reject(err);
|
|
3399
|
+
});
|
|
3400
|
+
});
|
|
3401
|
+
req.on("error", reject);
|
|
3402
|
+
req.setTimeout(3e4, () => {
|
|
3403
|
+
req.destroy(new Error(`Jira API ${method} ${path13} timed out after 30s`));
|
|
3404
|
+
});
|
|
3405
|
+
if (bodyStr) req.write(bodyStr);
|
|
3406
|
+
req.end();
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
});
|
|
3412
|
+
|
|
3413
|
+
// src/integrations/jira/workflow.ts
|
|
3414
|
+
function phaseFromJiraStatus(statusName, statusCategoryKey) {
|
|
3415
|
+
const nameKey = Object.keys(JIRA_STATUS_TO_PHASE).find(
|
|
3416
|
+
(k) => k.toLowerCase() === statusName.toLowerCase()
|
|
3417
|
+
);
|
|
3418
|
+
if (nameKey) return JIRA_STATUS_TO_PHASE[nameKey];
|
|
3419
|
+
if (statusCategoryKey) {
|
|
3420
|
+
return STATUS_CATEGORY_TO_PHASE[statusCategoryKey] ?? "TODO";
|
|
3421
|
+
}
|
|
3422
|
+
return "TODO";
|
|
3423
|
+
}
|
|
3424
|
+
function shouldPreserveLocalPhaseJira(localPhase, jiraStatusName) {
|
|
3425
|
+
const nameKey = Object.keys(JIRA_PHASE_GROUPS).find(
|
|
3426
|
+
(k) => k.toLowerCase() === jiraStatusName.toLowerCase()
|
|
3427
|
+
);
|
|
3428
|
+
const group = nameKey ? JIRA_PHASE_GROUPS[nameKey] : void 0;
|
|
3429
|
+
if (!group) return false;
|
|
3430
|
+
return group.includes(localPhase);
|
|
3431
|
+
}
|
|
3432
|
+
function resolveTransition(transitions, targetStatusName) {
|
|
3433
|
+
const exact = transitions.find(
|
|
3434
|
+
(t) => t.to.name.toLowerCase() === targetStatusName.toLowerCase()
|
|
3435
|
+
);
|
|
3436
|
+
if (exact) return exact.id;
|
|
3437
|
+
const targetCategory = statusNameToCategory(targetStatusName);
|
|
3438
|
+
if (targetCategory) {
|
|
3439
|
+
const catMatch = transitions.find(
|
|
3440
|
+
(t) => t.to.statusCategory.key === targetCategory
|
|
3441
|
+
);
|
|
3442
|
+
if (catMatch) return catMatch.id;
|
|
3443
|
+
}
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
function statusNameToCategory(statusName) {
|
|
3447
|
+
const lower = statusName.toLowerCase();
|
|
3448
|
+
if (["to do", "backlog", "open", "reopened"].includes(lower)) return "new";
|
|
3449
|
+
if (["in progress", "in review", "in development", "code review"].includes(lower)) return "indeterminate";
|
|
3450
|
+
if (["done", "closed", "resolved"].includes(lower)) return "done";
|
|
3451
|
+
return null;
|
|
3452
|
+
}
|
|
3453
|
+
var PHASE_TO_JIRA_STATUS, JIRA_STATUS_TO_PHASE, JIRA_PHASE_GROUPS, STATUS_CATEGORY_TO_PHASE;
|
|
3454
|
+
var init_workflow = __esm({
|
|
3455
|
+
"src/integrations/jira/workflow.ts"() {
|
|
3456
|
+
"use strict";
|
|
3457
|
+
PHASE_TO_JIRA_STATUS = {
|
|
3458
|
+
TODO: "To Do",
|
|
3459
|
+
IN_PROGRESS: "In Progress",
|
|
3460
|
+
AGENT_COMPLETE: "In Progress",
|
|
3461
|
+
AWAIT_HUMAN_ACTION: "In Progress",
|
|
3462
|
+
HUMAN_VERIFIED: "In Review",
|
|
3463
|
+
POST_WORK_COMPLETED: "Done",
|
|
3464
|
+
COMPLETE: "Done"
|
|
3465
|
+
};
|
|
3466
|
+
JIRA_STATUS_TO_PHASE = {
|
|
3467
|
+
"Backlog": "TODO",
|
|
3468
|
+
"To Do": "TODO",
|
|
3469
|
+
"Open": "TODO",
|
|
3470
|
+
"Reopened": "TODO",
|
|
3471
|
+
"Selected for Development": "TODO",
|
|
3472
|
+
"In Progress": "IN_PROGRESS",
|
|
3473
|
+
"In Development": "IN_PROGRESS",
|
|
3474
|
+
"In Review": "HUMAN_VERIFIED",
|
|
3475
|
+
"Code Review": "HUMAN_VERIFIED",
|
|
3476
|
+
"Done": "COMPLETE",
|
|
3477
|
+
"Closed": "COMPLETE",
|
|
3478
|
+
"Resolved": "COMPLETE"
|
|
3479
|
+
};
|
|
3480
|
+
JIRA_PHASE_GROUPS = {
|
|
3481
|
+
"Backlog": ["TODO"],
|
|
3482
|
+
"To Do": ["TODO"],
|
|
3483
|
+
"Open": ["TODO"],
|
|
3484
|
+
"Reopened": ["TODO"],
|
|
3485
|
+
"Selected for Development": ["TODO"],
|
|
3486
|
+
"In Progress": ["IN_PROGRESS", "AGENT_COMPLETE", "AWAIT_HUMAN_ACTION"],
|
|
3487
|
+
"In Development": ["IN_PROGRESS", "AGENT_COMPLETE", "AWAIT_HUMAN_ACTION"],
|
|
3488
|
+
"In Review": ["HUMAN_VERIFIED"],
|
|
3489
|
+
"Code Review": ["HUMAN_VERIFIED"],
|
|
3490
|
+
"Done": ["POST_WORK_COMPLETED", "COMPLETE"],
|
|
3491
|
+
"Closed": ["POST_WORK_COMPLETED", "COMPLETE"],
|
|
3492
|
+
"Resolved": ["POST_WORK_COMPLETED", "COMPLETE"]
|
|
3493
|
+
};
|
|
3494
|
+
STATUS_CATEGORY_TO_PHASE = {
|
|
3495
|
+
"new": "TODO",
|
|
3496
|
+
"indeterminate": "IN_PROGRESS",
|
|
3497
|
+
"done": "COMPLETE"
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
});
|
|
3501
|
+
|
|
3502
|
+
// src/integrations/jira/adf.ts
|
|
3503
|
+
function plainTextToAdf(text) {
|
|
3504
|
+
if (!text.trim()) {
|
|
3505
|
+
return { version: 1, type: "doc", content: [{ type: "paragraph", content: [] }] };
|
|
3506
|
+
}
|
|
3507
|
+
const paragraphs = text.split(/\n{2,}/);
|
|
3508
|
+
return {
|
|
3509
|
+
version: 1,
|
|
3510
|
+
type: "doc",
|
|
3511
|
+
content: paragraphs.map((p) => ({
|
|
3512
|
+
type: "paragraph",
|
|
3513
|
+
content: [{ type: "text", text: p.trim() }]
|
|
3514
|
+
}))
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
function markdownToAdf(md) {
|
|
3518
|
+
if (!md.trim()) {
|
|
3519
|
+
return { version: 1, type: "doc", content: [{ type: "paragraph", content: [] }] };
|
|
3520
|
+
}
|
|
3521
|
+
const content = [];
|
|
3522
|
+
const lines = md.split("\n");
|
|
3523
|
+
let i = 0;
|
|
3524
|
+
while (i < lines.length) {
|
|
3525
|
+
const line = lines[i];
|
|
3526
|
+
if (line.startsWith("```")) {
|
|
3527
|
+
const lang = line.slice(3).trim() || void 0;
|
|
3528
|
+
const codeLines = [];
|
|
3529
|
+
i++;
|
|
3530
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
3531
|
+
codeLines.push(lines[i]);
|
|
3532
|
+
i++;
|
|
3533
|
+
}
|
|
3534
|
+
i++;
|
|
3535
|
+
content.push({
|
|
3536
|
+
type: "codeBlock",
|
|
3537
|
+
...lang ? { attrs: { language: lang } } : {},
|
|
3538
|
+
content: [{ type: "text", text: codeLines.join("\n") }]
|
|
3539
|
+
});
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
3543
|
+
if (headingMatch) {
|
|
3544
|
+
const level = headingMatch[1].length;
|
|
3545
|
+
content.push({
|
|
3546
|
+
type: "heading",
|
|
3547
|
+
attrs: { level },
|
|
3548
|
+
content: [{ type: "text", text: headingMatch[2] }]
|
|
3549
|
+
});
|
|
3550
|
+
i++;
|
|
3551
|
+
continue;
|
|
3552
|
+
}
|
|
3553
|
+
if (!line.trim()) {
|
|
3554
|
+
i++;
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
const paragraphLines = [];
|
|
3558
|
+
while (i < lines.length && lines[i].trim() && !lines[i].startsWith("```") && !lines[i].match(/^#{1,6}\s/)) {
|
|
3559
|
+
paragraphLines.push(lines[i]);
|
|
3560
|
+
i++;
|
|
3561
|
+
}
|
|
3562
|
+
if (paragraphLines.length > 0) {
|
|
3563
|
+
content.push({
|
|
3564
|
+
type: "paragraph",
|
|
3565
|
+
content: [{ type: "text", text: paragraphLines.join("\n") }]
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
if (content.length === 0) {
|
|
3570
|
+
content.push({ type: "paragraph", content: [] });
|
|
3571
|
+
}
|
|
3572
|
+
return { version: 1, type: "doc", content };
|
|
3573
|
+
}
|
|
3574
|
+
function adfToPlainText(adf) {
|
|
3575
|
+
if (!adf?.content) return "";
|
|
3576
|
+
return adf.content.map(nodeToText).filter(Boolean).join("\n\n");
|
|
3577
|
+
}
|
|
3578
|
+
function nodeToText(node) {
|
|
3579
|
+
switch (node.type) {
|
|
3580
|
+
case "text":
|
|
3581
|
+
return node.text ?? "";
|
|
3582
|
+
case "paragraph":
|
|
3583
|
+
return childrenToText(node);
|
|
3584
|
+
case "heading": {
|
|
3585
|
+
const level = node.attrs?.level ?? 1;
|
|
3586
|
+
const prefix = "#".repeat(level);
|
|
3587
|
+
return `${prefix} ${childrenToText(node)}`;
|
|
3588
|
+
}
|
|
3589
|
+
case "codeBlock": {
|
|
3590
|
+
const lang = node.attrs?.language ?? "";
|
|
3591
|
+
return `\`\`\`${lang}
|
|
3592
|
+
${childrenToText(node)}
|
|
3593
|
+
\`\`\``;
|
|
3594
|
+
}
|
|
3595
|
+
case "hardBreak":
|
|
3596
|
+
return "\n";
|
|
3597
|
+
case "bulletList":
|
|
3598
|
+
case "orderedList":
|
|
3599
|
+
return (node.content ?? []).map((li, idx) => {
|
|
3600
|
+
const prefix = node.type === "orderedList" ? `${idx + 1}. ` : "- ";
|
|
3601
|
+
return `${prefix}${childrenToText(li)}`;
|
|
3602
|
+
}).join("\n");
|
|
3603
|
+
default:
|
|
3604
|
+
return childrenToText(node);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
function childrenToText(node) {
|
|
3608
|
+
if (!node.content) return node.text ?? "";
|
|
3609
|
+
return node.content.map(nodeToText).join("");
|
|
3610
|
+
}
|
|
3611
|
+
var init_adf = __esm({
|
|
3612
|
+
"src/integrations/jira/adf.ts"() {
|
|
3613
|
+
"use strict";
|
|
3614
|
+
}
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3617
|
+
// src/integrations/jira/sync.ts
|
|
3618
|
+
var sync_exports = {};
|
|
3619
|
+
__export(sync_exports, {
|
|
3620
|
+
autoPushTask: () => autoPushTask,
|
|
3621
|
+
deltaPull: () => deltaPull,
|
|
3622
|
+
fullPullAllIssues: () => fullPullAllIssues,
|
|
3623
|
+
getJiraSyncStatus: () => getJiraSyncStatus,
|
|
3624
|
+
isJiraPushSuccess: () => isJiraPushSuccess,
|
|
3625
|
+
mapToLocal: () => mapToLocal,
|
|
3626
|
+
syncTasks: () => syncTasks
|
|
3627
|
+
});
|
|
3628
|
+
import path12 from "path";
|
|
3629
|
+
function getJiraConfig(config) {
|
|
3630
|
+
return config.plugins?.jira;
|
|
3631
|
+
}
|
|
3632
|
+
function ext(task) {
|
|
3633
|
+
return task.ext?.jira;
|
|
3634
|
+
}
|
|
3635
|
+
function mapToLocal(remote, config) {
|
|
3636
|
+
const jiraConfig = getJiraConfig(config);
|
|
3637
|
+
const statusName = remote.fields.status.name;
|
|
3638
|
+
const statusCategoryKey = remote.fields.status.statusCategory?.key;
|
|
3639
|
+
const phase = phaseFromJiraStatus(statusName, statusCategoryKey);
|
|
3640
|
+
const status = deriveStatusFromPhase(phase);
|
|
3641
|
+
let priority = "none";
|
|
3642
|
+
if (remote.fields.priority?.name) {
|
|
3643
|
+
priority = JIRA_TO_LOCAL_PRIORITY[remote.fields.priority.name] ?? "none";
|
|
3644
|
+
}
|
|
3645
|
+
let project = remote.fields.project.key;
|
|
3646
|
+
if (jiraConfig.project_mapping) {
|
|
3647
|
+
const entry = Object.entries(jiraConfig.project_mapping).find(
|
|
3648
|
+
([, jiraKey]) => jiraKey === remote.fields.project.key
|
|
3649
|
+
);
|
|
3650
|
+
if (entry) project = entry[0];
|
|
3651
|
+
}
|
|
3652
|
+
const description = adfToPlainText(remote.fields.description);
|
|
3653
|
+
return {
|
|
3654
|
+
title: remote.fields.summary,
|
|
3655
|
+
status,
|
|
3656
|
+
phase,
|
|
3657
|
+
priority,
|
|
3658
|
+
project,
|
|
3659
|
+
source: "jira",
|
|
3660
|
+
ext: {
|
|
3661
|
+
jira: {
|
|
3662
|
+
issue_id: remote.id,
|
|
3663
|
+
issue_key: remote.key,
|
|
3664
|
+
project_key: remote.fields.project.key,
|
|
3665
|
+
status_name: statusName
|
|
3666
|
+
}
|
|
3667
|
+
},
|
|
3668
|
+
external_url: `${jiraConfig.base_url}/browse/${remote.key}`,
|
|
3669
|
+
description,
|
|
3670
|
+
due_date: remote.fields.duedate ?? void 0,
|
|
3671
|
+
created_at: remote.fields.created,
|
|
3672
|
+
updated_at: remote.fields.updated
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
function composeCommentBody(task) {
|
|
3676
|
+
const headers = [];
|
|
3677
|
+
if (task.parent_task_id) headers.push(`Parent: ${task.parent_task_id.slice(0, 8)}`);
|
|
3678
|
+
if (task.unread) headers.push(`Attention: true`);
|
|
3679
|
+
if (task.depends_on?.length) headers.push(`DependsOn: ${task.depends_on.map((id) => id.slice(0, 8)).join(",")}`);
|
|
3680
|
+
const sections = [];
|
|
3681
|
+
if (task.summary) sections.push(`## Summary
|
|
3682
|
+
${task.summary}`);
|
|
3683
|
+
if (task.note) sections.push(`## Notes
|
|
3684
|
+
${task.note}`);
|
|
3685
|
+
if (task.conversation_log) sections.push(`## Conversation Log
|
|
3686
|
+
${task.conversation_log}`);
|
|
3687
|
+
const body = sections.join("\n\n");
|
|
3688
|
+
if (headers.length > 0 && body) return headers.join("\n") + "\n\n" + body;
|
|
3689
|
+
if (headers.length > 0) return headers.join("\n");
|
|
3690
|
+
return body;
|
|
3691
|
+
}
|
|
3692
|
+
function isJiraPushSuccess(r) {
|
|
3693
|
+
return "jiraIssueId" in r;
|
|
3694
|
+
}
|
|
3695
|
+
async function autoPushTask(task) {
|
|
3696
|
+
try {
|
|
3697
|
+
const config = await getConfig();
|
|
3698
|
+
const jiraConfig = getJiraConfig(config);
|
|
3699
|
+
if (!jiraConfig?.base_url) {
|
|
3700
|
+
return { error: "Jira base_url not configured" };
|
|
3701
|
+
}
|
|
3702
|
+
const client = new JiraClient(jiraConfig);
|
|
3703
|
+
const je = ext(task);
|
|
3704
|
+
const issueKey = je?.issue_key;
|
|
3705
|
+
const issueId = je?.issue_id;
|
|
3706
|
+
const statusName = je?.status_name;
|
|
3707
|
+
const commentId_raw = je?.comment_id;
|
|
3708
|
+
if (issueKey) {
|
|
3709
|
+
const updateInput = {
|
|
3710
|
+
fields: {
|
|
3711
|
+
summary: task.title,
|
|
3712
|
+
description: task.description ? plainTextToAdf(task.description) : void 0,
|
|
3713
|
+
priority: task.priority !== "none" ? { name: LOCAL_TO_JIRA_PRIORITY[task.priority] } : void 0,
|
|
3714
|
+
duedate: task.due_date ?? null
|
|
3715
|
+
}
|
|
3716
|
+
};
|
|
3717
|
+
await client.updateIssue(issueKey, updateInput);
|
|
3718
|
+
const targetStatus = PHASE_TO_JIRA_STATUS[task.phase];
|
|
3719
|
+
if (targetStatus && targetStatus.toLowerCase() !== (statusName ?? "").toLowerCase()) {
|
|
3720
|
+
try {
|
|
3721
|
+
const { transitions } = await client.getTransitions(issueKey);
|
|
3722
|
+
const transitionId = resolveTransition(transitions, targetStatus);
|
|
3723
|
+
if (transitionId) {
|
|
3724
|
+
await client.doTransition(issueKey, transitionId);
|
|
3725
|
+
syncLog.debug("transitioned Jira issue", { key: issueKey, to: targetStatus });
|
|
3726
|
+
}
|
|
3727
|
+
} catch (err) {
|
|
3728
|
+
syncLog.debug("failed to transition Jira issue", {
|
|
3729
|
+
key: issueKey,
|
|
3730
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
const commentBody = composeCommentBody(task);
|
|
3735
|
+
let commentId = commentId_raw;
|
|
3736
|
+
if (commentBody) {
|
|
3737
|
+
const adfBody = markdownToAdf(commentBody);
|
|
3738
|
+
if (commentId) {
|
|
3739
|
+
try {
|
|
3740
|
+
await client.updateComment(issueKey, commentId, adfBody);
|
|
3741
|
+
} catch {
|
|
3742
|
+
try {
|
|
3743
|
+
const newComment = await client.addComment(issueKey, adfBody);
|
|
3744
|
+
commentId = newComment.id;
|
|
3745
|
+
} catch {
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
} else {
|
|
3749
|
+
try {
|
|
3750
|
+
const newComment = await client.addComment(issueKey, adfBody);
|
|
3751
|
+
commentId = newComment.id;
|
|
3752
|
+
} catch {
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
let serverTimestamp;
|
|
3757
|
+
try {
|
|
3758
|
+
const freshIssue = await client.getIssue(issueKey);
|
|
3759
|
+
serverTimestamp = freshIssue.fields.updated;
|
|
3760
|
+
} catch {
|
|
3761
|
+
}
|
|
3762
|
+
syncLog.info("pushed task update to Jira", { key: issueKey, title: task.title });
|
|
3763
|
+
return { jiraIssueId: issueId ?? "", jiraIssueKey: issueKey, commentId, serverTimestamp };
|
|
3764
|
+
} else {
|
|
3765
|
+
const projectKey = resolveProjectKey(task, config);
|
|
3766
|
+
const createInput = {
|
|
3767
|
+
fields: {
|
|
3768
|
+
project: { key: projectKey },
|
|
3769
|
+
summary: task.title,
|
|
3770
|
+
issuetype: { name: jiraConfig.issue_type ?? "Task" },
|
|
3771
|
+
description: task.description ? plainTextToAdf(task.description) : void 0,
|
|
3772
|
+
priority: task.priority !== "none" ? { name: LOCAL_TO_JIRA_PRIORITY[task.priority] } : void 0,
|
|
3773
|
+
duedate: task.due_date
|
|
3774
|
+
}
|
|
3775
|
+
};
|
|
3776
|
+
const created = await client.createIssue(createInput);
|
|
3777
|
+
let commentId;
|
|
3778
|
+
const commentBody = composeCommentBody(task);
|
|
3779
|
+
if (commentBody) {
|
|
3780
|
+
try {
|
|
3781
|
+
const comment = await client.addComment(created.key, markdownToAdf(commentBody));
|
|
3782
|
+
commentId = comment.id;
|
|
3783
|
+
} catch {
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
let serverTimestamp;
|
|
3787
|
+
try {
|
|
3788
|
+
const freshIssue = await client.getIssue(created.key);
|
|
3789
|
+
serverTimestamp = freshIssue.fields.updated;
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
syncLog.info("created issue in Jira", { key: created.key, title: task.title });
|
|
3793
|
+
return { jiraIssueId: created.id, jiraIssueKey: created.key, commentId, serverTimestamp };
|
|
3794
|
+
}
|
|
3795
|
+
} catch (err) {
|
|
3796
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3797
|
+
syncLog.error("failed to push task to Jira", {
|
|
3798
|
+
taskId: task.id,
|
|
3799
|
+
error: message
|
|
3800
|
+
});
|
|
3801
|
+
return { error: message };
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
async function deltaPull(updateLocalTask, addLocalTask) {
|
|
3805
|
+
const config = await getConfig();
|
|
3806
|
+
const jiraConfig = getJiraConfig(config);
|
|
3807
|
+
if (!jiraConfig?.base_url) return false;
|
|
3808
|
+
const client = new JiraClient(jiraConfig);
|
|
3809
|
+
const syncState = await readJsonFile(JIRA_SYNC_FILE, { lastSync: "" });
|
|
3810
|
+
const jqlParts = [];
|
|
3811
|
+
jqlParts.push(`project = "${jiraConfig.project_key}"`);
|
|
3812
|
+
if (syncState.lastSync) {
|
|
3813
|
+
const since = formatJqlDate(syncState.lastSync);
|
|
3814
|
+
jqlParts.push(`updated >= "${since}"`);
|
|
3815
|
+
}
|
|
3816
|
+
if (jiraConfig.assignee_filter) {
|
|
3817
|
+
if (jiraConfig.assignee_filter === "currentUser") {
|
|
3818
|
+
jqlParts.push("assignee = currentUser()");
|
|
3819
|
+
} else {
|
|
3820
|
+
jqlParts.push(`assignee = "${jiraConfig.assignee_filter}"`);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
if (jiraConfig.jql_filter) {
|
|
3824
|
+
jqlParts.push(`(${jiraConfig.jql_filter})`);
|
|
3825
|
+
}
|
|
3826
|
+
const jql = jqlParts.join(" AND ") + " ORDER BY updated DESC";
|
|
3827
|
+
const searchResult = await client.searchIssues(jql, {
|
|
3828
|
+
maxResults: 100,
|
|
3829
|
+
fields: [
|
|
3830
|
+
"summary",
|
|
3831
|
+
"description",
|
|
3832
|
+
"status",
|
|
3833
|
+
"priority",
|
|
3834
|
+
"project",
|
|
3835
|
+
"issuetype",
|
|
3836
|
+
"created",
|
|
3837
|
+
"updated",
|
|
3838
|
+
"duedate",
|
|
3839
|
+
"assignee",
|
|
3840
|
+
"comment",
|
|
3841
|
+
"subtasks",
|
|
3842
|
+
"parent"
|
|
3843
|
+
]
|
|
3844
|
+
});
|
|
3845
|
+
if (searchResult.issues.length === 0) {
|
|
3846
|
+
syncState.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
3847
|
+
await writeJsonFile(JIRA_SYNC_FILE, syncState);
|
|
3848
|
+
return false;
|
|
3849
|
+
}
|
|
3850
|
+
let hasChanges = false;
|
|
3851
|
+
for (const remote of searchResult.issues) {
|
|
3852
|
+
const existing = await findTaskByExtId("jira", remote.key);
|
|
3853
|
+
if (existing) {
|
|
3854
|
+
const remoteUpdated = new Date(remote.fields.updated).getTime();
|
|
3855
|
+
const syncedAt = existing._syncedAt ? new Date(existing._syncedAt).getTime() : 0;
|
|
3856
|
+
if (remoteUpdated > syncedAt) {
|
|
3857
|
+
const updates = mapToLocal(remote, config);
|
|
3858
|
+
delete updates.source;
|
|
3859
|
+
delete updates.note;
|
|
3860
|
+
delete updates.summary;
|
|
3861
|
+
delete updates.conversation_log;
|
|
3862
|
+
const remoteStatusName = remote.fields.status.name;
|
|
3863
|
+
if (existing.phase && updates.phase) {
|
|
3864
|
+
if (shouldPreserveLocalPhaseJira(existing.phase, remoteStatusName)) {
|
|
3865
|
+
delete updates.phase;
|
|
3866
|
+
delete updates.status;
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
await updateLocalTask(existing.id, updates);
|
|
3870
|
+
bus.emit(EventNames.TASK_UPDATED, { task: { ...existing, ...updates } }, ["web-ui"], { source: "jira-sync" });
|
|
3871
|
+
hasChanges = true;
|
|
3872
|
+
syncLog.debug("updated local task from Jira", { key: remote.key, title: remote.fields.summary });
|
|
3873
|
+
}
|
|
3874
|
+
} else {
|
|
3875
|
+
const partial = mapToLocal(remote, config);
|
|
3876
|
+
const now = remote.fields.created || (/* @__PURE__ */ new Date()).toISOString();
|
|
3877
|
+
const newTask = await addLocalTask({
|
|
3878
|
+
title: partial.title ?? remote.fields.summary,
|
|
3879
|
+
status: partial.status ?? "todo",
|
|
3880
|
+
phase: partial.phase ?? "TODO",
|
|
3881
|
+
priority: partial.priority ?? "none",
|
|
3882
|
+
// Falls back to the reserved project so a provider task never lands in
|
|
3883
|
+
// Inbox, which is structurally local-only.
|
|
3884
|
+
project: partial.project ?? jiraConfig.project,
|
|
3885
|
+
source: "jira",
|
|
3886
|
+
ext: partial.ext,
|
|
3887
|
+
external_url: partial.external_url,
|
|
3888
|
+
session_ids: [],
|
|
3889
|
+
created_at: now,
|
|
3890
|
+
updated_at: remote.fields.updated || now,
|
|
3891
|
+
due_date: partial.due_date,
|
|
3892
|
+
description: partial.description ?? "",
|
|
3893
|
+
summary: "",
|
|
3894
|
+
note: ""
|
|
3895
|
+
});
|
|
3896
|
+
bus.emit(EventNames.TASK_CREATED, { task: newTask }, ["web-ui"], { source: "jira-sync" });
|
|
3897
|
+
hasChanges = true;
|
|
3898
|
+
syncLog.debug("created local task from Jira", { key: remote.key, title: remote.fields.summary });
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
syncState.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
3902
|
+
await writeJsonFile(JIRA_SYNC_FILE, syncState);
|
|
3903
|
+
if (hasChanges) {
|
|
3904
|
+
syncLog.info(`Jira sync: pulled ${searchResult.issues.length} issues`);
|
|
3905
|
+
}
|
|
3906
|
+
return hasChanges;
|
|
3907
|
+
}
|
|
3908
|
+
async function fullPullAllIssues() {
|
|
3909
|
+
const config = await getConfig();
|
|
3910
|
+
const jiraConfig = getJiraConfig(config);
|
|
3911
|
+
if (!jiraConfig?.base_url) return [];
|
|
3912
|
+
const client = new JiraClient(jiraConfig);
|
|
3913
|
+
const jqlParts = [];
|
|
3914
|
+
jqlParts.push(`project = "${jiraConfig.project_key}"`);
|
|
3915
|
+
if (jiraConfig.assignee_filter) {
|
|
3916
|
+
if (jiraConfig.assignee_filter === "currentUser") {
|
|
3917
|
+
jqlParts.push("assignee = currentUser()");
|
|
3918
|
+
} else {
|
|
3919
|
+
jqlParts.push(`assignee = "${jiraConfig.assignee_filter}"`);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
if (jiraConfig.jql_filter) {
|
|
3923
|
+
jqlParts.push(`(${jiraConfig.jql_filter})`);
|
|
3924
|
+
}
|
|
3925
|
+
const jql = jqlParts.join(" AND ") + " ORDER BY updated DESC";
|
|
3926
|
+
const jqlFields = [
|
|
3927
|
+
"summary",
|
|
3928
|
+
"description",
|
|
3929
|
+
"status",
|
|
3930
|
+
"priority",
|
|
3931
|
+
"project",
|
|
3932
|
+
"issuetype",
|
|
3933
|
+
"created",
|
|
3934
|
+
"updated",
|
|
3935
|
+
"duedate",
|
|
3936
|
+
"assignee",
|
|
3937
|
+
"parent"
|
|
3938
|
+
];
|
|
3939
|
+
const result = [];
|
|
3940
|
+
let startAt = 0;
|
|
3941
|
+
const maxPerPage = 100;
|
|
3942
|
+
let total = Infinity;
|
|
3943
|
+
while (startAt < total) {
|
|
3944
|
+
const searchResult = await client.searchIssues(jql, {
|
|
3945
|
+
maxResults: maxPerPage,
|
|
3946
|
+
startAt,
|
|
3947
|
+
fields: jqlFields
|
|
3948
|
+
});
|
|
3949
|
+
total = searchResult.total;
|
|
3950
|
+
for (const issue of searchResult.issues) {
|
|
3951
|
+
const fields = mapToLocal(issue, config);
|
|
3952
|
+
result.push({
|
|
3953
|
+
remoteId: issue.key,
|
|
3954
|
+
title: issue.fields.summary,
|
|
3955
|
+
remoteUpdatedAt: issue.fields.updated,
|
|
3956
|
+
fields
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
startAt += searchResult.issues.length;
|
|
3960
|
+
if (searchResult.issues.length === 0) break;
|
|
3961
|
+
}
|
|
3962
|
+
return result;
|
|
3963
|
+
}
|
|
3964
|
+
async function syncTasks(updateLocalTask, addLocalTask, updateTaskRaw) {
|
|
3965
|
+
let pushed = 0;
|
|
3966
|
+
const config = await getConfig();
|
|
3967
|
+
const jiraConfig = getJiraConfig(config);
|
|
3968
|
+
const unsynced = await listUnsyncedTasks("jira");
|
|
3969
|
+
for (const task of unsynced) {
|
|
3970
|
+
const result = await autoPushTask(task);
|
|
3971
|
+
if (isJiraPushSuccess(result)) {
|
|
3972
|
+
await updateTaskRaw(task.id, {
|
|
3973
|
+
ext: {
|
|
3974
|
+
jira: {
|
|
3975
|
+
issue_id: result.jiraIssueId,
|
|
3976
|
+
issue_key: result.jiraIssueKey,
|
|
3977
|
+
...result.commentId ? { comment_id: result.commentId } : {}
|
|
3978
|
+
}
|
|
3979
|
+
},
|
|
3980
|
+
external_url: jiraConfig ? `${jiraConfig.base_url}/browse/${result.jiraIssueKey}` : ""
|
|
3981
|
+
});
|
|
3982
|
+
pushed++;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
const pulled = await deltaPull(updateLocalTask, addLocalTask);
|
|
3986
|
+
return { pushed, pulled };
|
|
3987
|
+
}
|
|
3988
|
+
async function getJiraSyncStatus() {
|
|
3989
|
+
const config = await getConfig();
|
|
3990
|
+
const jiraConfig = getJiraConfig(config);
|
|
3991
|
+
if (!jiraConfig?.base_url) {
|
|
3992
|
+
return { configured: false, hasCredentials: false };
|
|
3993
|
+
}
|
|
3994
|
+
return {
|
|
3995
|
+
configured: true,
|
|
3996
|
+
hasCredentials: !!jiraConfig.auth?.token,
|
|
3997
|
+
baseUrl: jiraConfig.base_url,
|
|
3998
|
+
projectKey: jiraConfig.project_key,
|
|
3999
|
+
project: jiraConfig.project
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
function resolveProjectKey(task, config) {
|
|
4003
|
+
const jiraConfig = getJiraConfig(config);
|
|
4004
|
+
if (jiraConfig.project_mapping && task.project) {
|
|
4005
|
+
const mapped = jiraConfig.project_mapping[task.project];
|
|
4006
|
+
if (mapped) return mapped;
|
|
4007
|
+
}
|
|
4008
|
+
return jiraConfig.project_key;
|
|
4009
|
+
}
|
|
4010
|
+
function formatJqlDate(iso) {
|
|
4011
|
+
const d = new Date(iso);
|
|
4012
|
+
const yyyy = d.getFullYear();
|
|
4013
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
4014
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
4015
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
4016
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
4017
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
|
|
4018
|
+
}
|
|
4019
|
+
var JIRA_SYNC_FILE, syncLog, LOCAL_TO_JIRA_PRIORITY, JIRA_TO_LOCAL_PRIORITY;
|
|
4020
|
+
var init_sync = __esm({
|
|
4021
|
+
"src/integrations/jira/sync.ts"() {
|
|
4022
|
+
"use strict";
|
|
4023
|
+
init_constants();
|
|
4024
|
+
init_fs();
|
|
4025
|
+
init_config_manager();
|
|
4026
|
+
init_logging();
|
|
4027
|
+
init_event_bus();
|
|
4028
|
+
init_phase();
|
|
4029
|
+
init_task_manager();
|
|
4030
|
+
init_jira_client();
|
|
4031
|
+
init_workflow();
|
|
4032
|
+
init_adf();
|
|
4033
|
+
JIRA_SYNC_FILE = path12.join(SYNC_DIR, "jira-sync.json");
|
|
4034
|
+
syncLog = createSubsystemLogger("jira");
|
|
4035
|
+
LOCAL_TO_JIRA_PRIORITY = {
|
|
4036
|
+
immediate: "Highest",
|
|
4037
|
+
important: "High",
|
|
4038
|
+
backlog: "Low",
|
|
4039
|
+
none: "Medium"
|
|
4040
|
+
};
|
|
4041
|
+
JIRA_TO_LOCAL_PRIORITY = {
|
|
4042
|
+
"Highest": "immediate",
|
|
4043
|
+
"High": "important",
|
|
4044
|
+
"Medium": "none",
|
|
4045
|
+
"Low": "backlog",
|
|
4046
|
+
"Lowest": "backlog"
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
});
|
|
4050
|
+
|
|
4051
|
+
// src/integrations/jira/index.ts
|
|
4052
|
+
function register(api) {
|
|
4053
|
+
const config = api.config;
|
|
4054
|
+
const reservedProject = config.project || "";
|
|
4055
|
+
const sync = {
|
|
4056
|
+
async createTask(task) {
|
|
4057
|
+
const { autoPushTask: autoPushTask2, isJiraPushSuccess: isJiraPushSuccess2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4058
|
+
const result = await autoPushTask2(task);
|
|
4059
|
+
if (isJiraPushSuccess2(result)) {
|
|
4060
|
+
return {
|
|
4061
|
+
jira: {
|
|
4062
|
+
issue_id: result.jiraIssueId,
|
|
4063
|
+
issue_key: result.jiraIssueKey,
|
|
4064
|
+
comment_id: result.commentId
|
|
4065
|
+
}
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
if (result.error) throw new Error(result.error);
|
|
4069
|
+
return null;
|
|
4070
|
+
},
|
|
4071
|
+
async deleteTask(_task) {
|
|
4072
|
+
},
|
|
4073
|
+
async updateTitle(task) {
|
|
4074
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4075
|
+
await autoPushTask2(task);
|
|
4076
|
+
},
|
|
4077
|
+
async updateDescription(task) {
|
|
4078
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4079
|
+
await autoPushTask2(task);
|
|
4080
|
+
},
|
|
4081
|
+
async updateSummary(task) {
|
|
4082
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4083
|
+
await autoPushTask2(task);
|
|
4084
|
+
},
|
|
4085
|
+
async updateNote(task) {
|
|
4086
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4087
|
+
await autoPushTask2(task);
|
|
4088
|
+
},
|
|
4089
|
+
async updateConversationLog(task) {
|
|
4090
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4091
|
+
await autoPushTask2(task);
|
|
4092
|
+
},
|
|
4093
|
+
async updatePriority(task) {
|
|
4094
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4095
|
+
await autoPushTask2(task);
|
|
4096
|
+
},
|
|
4097
|
+
async updatePhase(task) {
|
|
4098
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4099
|
+
await autoPushTask2(task);
|
|
4100
|
+
},
|
|
4101
|
+
async updateDueDate(task) {
|
|
4102
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4103
|
+
await autoPushTask2(task);
|
|
4104
|
+
},
|
|
4105
|
+
async updateStar(_task) {
|
|
4106
|
+
},
|
|
4107
|
+
async updateProject(task) {
|
|
4108
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4109
|
+
await autoPushTask2(task);
|
|
4110
|
+
},
|
|
4111
|
+
async updateDependencies(task) {
|
|
4112
|
+
const { autoPushTask: autoPushTask2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4113
|
+
await autoPushTask2(task);
|
|
4114
|
+
},
|
|
4115
|
+
async pushTask(task) {
|
|
4116
|
+
const { autoPushTask: autoPushTask2, isJiraPushSuccess: isJiraPushSuccess2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4117
|
+
const result = await autoPushTask2(task);
|
|
4118
|
+
if (isJiraPushSuccess2(result)) {
|
|
4119
|
+
return {
|
|
4120
|
+
serverTimestamp: result.serverTimestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4121
|
+
ext: {
|
|
4122
|
+
jira: {
|
|
4123
|
+
issue_id: result.jiraIssueId,
|
|
4124
|
+
issue_key: result.jiraIssueKey,
|
|
4125
|
+
comment_id: result.commentId
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
throw new Error(result.error);
|
|
4131
|
+
},
|
|
4132
|
+
async associateSubtask(_parent, _child) {
|
|
4133
|
+
},
|
|
4134
|
+
async disassociateSubtask(_parent, _child) {
|
|
4135
|
+
},
|
|
4136
|
+
async syncPoll(ctx) {
|
|
4137
|
+
const { deltaPull: deltaPull2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4138
|
+
await deltaPull2(
|
|
4139
|
+
async (id, updates) => {
|
|
4140
|
+
await ctx.updateTask(id, updates);
|
|
4141
|
+
},
|
|
4142
|
+
async (taskData) => {
|
|
4143
|
+
const t = await ctx.addTask(taskData);
|
|
4144
|
+
return t;
|
|
4145
|
+
}
|
|
4146
|
+
);
|
|
4147
|
+
},
|
|
4148
|
+
async fullPull() {
|
|
4149
|
+
const { fullPullAllIssues: fullPullAllIssues2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
4150
|
+
return fullPullAllIssues2();
|
|
4151
|
+
},
|
|
4152
|
+
extractRemoteId(task) {
|
|
4153
|
+
return task.ext?.jira?.issue_key;
|
|
4154
|
+
}
|
|
4155
|
+
};
|
|
4156
|
+
api.registerSync(sync);
|
|
4157
|
+
api.registerExtIndex({
|
|
4158
|
+
source: "jira",
|
|
4159
|
+
paths: [{ key: "issue_key", json: "$.jira.issue_key" }]
|
|
4160
|
+
});
|
|
4161
|
+
api.registerSourceClaim((project) => {
|
|
4162
|
+
return reservedProject ? project.toLowerCase() === reservedProject.toLowerCase() : false;
|
|
4163
|
+
}, { priority: 0 });
|
|
4164
|
+
api.registerDisplay({
|
|
4165
|
+
badge: "J",
|
|
4166
|
+
badgeColor: "#0052CC",
|
|
4167
|
+
externalLinkLabel: "Jira",
|
|
4168
|
+
getExternalUrl: (task) => task.external_url || null,
|
|
4169
|
+
isSynced: (task) => !!task.ext?.jira?.issue_key,
|
|
4170
|
+
syncTooltip: (task) => task.sync_error ? `Sync error: ${task.sync_error}` : "Synced to Jira"
|
|
4171
|
+
});
|
|
4172
|
+
api.registerAgentContext(
|
|
4173
|
+
'Tasks with source "jira" sync bidirectionally with Jira. Phase maps to Jira workflow transitions (To Do \u2192 In Progress \u2192 In Review \u2192 Done).'
|
|
4174
|
+
);
|
|
4175
|
+
api.registerMigration((tasks) => {
|
|
4176
|
+
for (const task of tasks) {
|
|
4177
|
+
const raw = task;
|
|
4178
|
+
if (raw.jira_issue_id && !task.ext?.jira) {
|
|
4179
|
+
if (!task.ext) task.ext = {};
|
|
4180
|
+
task.ext.jira = {
|
|
4181
|
+
issue_id: raw.jira_issue_id,
|
|
4182
|
+
issue_key: raw.jira_issue_key,
|
|
4183
|
+
project_key: raw.jira_project_key,
|
|
4184
|
+
comment_id: raw.jira_comment_id,
|
|
4185
|
+
status_name: raw.jira_status_name
|
|
4186
|
+
};
|
|
4187
|
+
delete raw.jira_issue_id;
|
|
4188
|
+
delete raw.jira_issue_key;
|
|
4189
|
+
delete raw.jira_project_key;
|
|
4190
|
+
delete raw.jira_comment_id;
|
|
4191
|
+
delete raw.jira_status_name;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
return tasks;
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
export {
|
|
4198
|
+
register as default
|
|
4199
|
+
};
|
|
4200
|
+
//# sourceMappingURL=index.js.map
|