@kkelly-offical/kkcode 0.1.2 → 0.1.6

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.
Files changed (58) hide show
  1. package/README.md +120 -178
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +41 -0
  4. package/src/agent/prompt/frontend-designer.txt +58 -0
  5. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  6. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  7. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  8. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  9. package/src/config/defaults.mjs +260 -195
  10. package/src/config/schema.mjs +71 -6
  11. package/src/core/constants.mjs +91 -46
  12. package/src/index.mjs +1 -1
  13. package/src/knowledge/frontend-aesthetics.txt +39 -0
  14. package/src/knowledge/loader.mjs +2 -1
  15. package/src/knowledge/tailwind.txt +12 -3
  16. package/src/mcp/client-http.mjs +141 -157
  17. package/src/mcp/client-sse.mjs +288 -286
  18. package/src/mcp/client-stdio.mjs +533 -451
  19. package/src/mcp/constants.mjs +2 -0
  20. package/src/mcp/registry.mjs +479 -394
  21. package/src/mcp/stdio-framing.mjs +133 -127
  22. package/src/mcp/tool-result.mjs +24 -0
  23. package/src/observability/index.mjs +42 -0
  24. package/src/observability/metrics.mjs +137 -0
  25. package/src/observability/tracer.mjs +137 -0
  26. package/src/orchestration/background-manager.mjs +372 -358
  27. package/src/orchestration/background-worker.mjs +305 -245
  28. package/src/orchestration/longagent-manager.mjs +171 -116
  29. package/src/orchestration/stage-scheduler.mjs +728 -489
  30. package/src/permission/exec-policy.mjs +9 -11
  31. package/src/provider/anthropic.mjs +1 -0
  32. package/src/provider/openai.mjs +340 -339
  33. package/src/provider/retry-policy.mjs +68 -68
  34. package/src/provider/router.mjs +241 -228
  35. package/src/provider/sse.mjs +104 -91
  36. package/src/repl.mjs +1 -1
  37. package/src/session/checkpoint.mjs +66 -3
  38. package/src/session/engine.mjs +227 -225
  39. package/src/session/longagent-4stage.mjs +460 -0
  40. package/src/session/longagent-hybrid.mjs +1081 -0
  41. package/src/session/longagent-plan.mjs +365 -329
  42. package/src/session/longagent-project-memory.mjs +53 -0
  43. package/src/session/longagent-scaffold.mjs +291 -100
  44. package/src/session/longagent-task-bus.mjs +54 -0
  45. package/src/session/longagent-utils.mjs +472 -0
  46. package/src/session/longagent.mjs +884 -1462
  47. package/src/session/project-context.mjs +30 -0
  48. package/src/session/store.mjs +510 -503
  49. package/src/session/task-validator.mjs +4 -3
  50. package/src/skill/builtin/design.mjs +76 -0
  51. package/src/skill/builtin/frontend.mjs +8 -0
  52. package/src/skill/registry.mjs +390 -336
  53. package/src/storage/ghost-commit-store.mjs +18 -8
  54. package/src/tool/executor.mjs +11 -0
  55. package/src/tool/git-auto.mjs +0 -19
  56. package/src/tool/registry.mjs +71 -37
  57. package/src/ui/activity-renderer.mjs +664 -410
  58. package/src/util/git.mjs +23 -0
@@ -1,116 +1,171 @@
1
- import path from "node:path"
2
- import { mkdir, writeFile, unlink, stat } from "node:fs/promises"
3
- import { readJson, writeJson } from "../storage/json-store.mjs"
4
- import { projectRootDir } from "../storage/paths.mjs"
5
-
6
- function statePath(cwd = process.cwd()) {
7
- return path.join(projectRootDir(cwd), "longagent-state.json")
8
- }
9
-
10
- function lockPath(cwd = process.cwd()) {
11
- return statePath(cwd) + ".lock"
12
- }
13
-
14
- async function ensure(cwd = process.cwd()) {
15
- await mkdir(projectRootDir(cwd), { recursive: true })
16
- }
17
-
18
- const LOCK_TIMEOUT_MS = 5000
19
- const LOCK_RETRY_MS = 50
20
-
21
- async function acquireLock(cwd) {
22
- const file = lockPath(cwd)
23
- const deadline = Date.now() + LOCK_TIMEOUT_MS
24
- while (Date.now() < deadline) {
25
- try {
26
- await writeFile(file, String(process.pid), { flag: "wx" })
27
- return true
28
- } catch (err) {
29
- if (err.code !== "EEXIST") throw err
30
- // Stale lock detection: if lock file is older than timeout, remove it
31
- try {
32
- const info = await stat(file)
33
- if (Date.now() - info.mtimeMs > LOCK_TIMEOUT_MS) {
34
- await unlink(file).catch(() => {})
35
- continue
36
- }
37
- } catch { /* lock disappeared, retry */ continue }
38
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS))
39
- }
40
- }
41
- // Timeout: force-remove stale lock and proceed
42
- await unlink(file).catch(() => {})
43
- return false
44
- }
45
-
46
- async function releaseLock(cwd) {
47
- await unlink(lockPath(cwd)).catch(() => {})
48
- }
49
-
50
- async function read(cwd = process.cwd()) {
51
- await ensure(cwd)
52
- return readJson(statePath(cwd), { sessions: {} })
53
- }
54
-
55
- async function write(data, cwd = process.cwd()) {
56
- await ensure(cwd)
57
- await writeJson(statePath(cwd), data)
58
- }
59
-
60
- export const LongAgentManager = {
61
- async update(sessionId, patch, cwd = process.cwd()) {
62
- await acquireLock(cwd)
63
- try {
64
- const state = await read(cwd)
65
- const current = state.sessions[sessionId] || {
66
- sessionId,
67
- status: "idle",
68
- phase: "L0",
69
- gateStatus: {},
70
- currentGate: "execution",
71
- recoveryCount: 0,
72
- planFrozen: false,
73
- currentStageId: null,
74
- stageIndex: 0,
75
- stageCount: 0,
76
- stageStatus: null,
77
- taskProgress: {},
78
- remainingFiles: [],
79
- remainingFilesCount: 0,
80
- lastGateFailures: [],
81
- createdAt: Date.now(),
82
- updatedAt: Date.now(),
83
- heartbeatAt: null,
84
- iterations: 0,
85
- lastMessage: ""
86
- }
87
- state.sessions[sessionId] = {
88
- ...current,
89
- ...patch,
90
- updatedAt: Date.now()
91
- }
92
- await write(state, cwd)
93
- return state.sessions[sessionId]
94
- } finally {
95
- await releaseLock(cwd)
96
- }
97
- },
98
- async get(sessionId, cwd = process.cwd()) {
99
- const state = await read(cwd)
100
- return state.sessions[sessionId] || null
101
- },
102
- async list(cwd = process.cwd()) {
103
- const state = await read(cwd)
104
- return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
105
- },
106
- async stop(sessionId, cwd = process.cwd()) {
107
- const existing = await this.get(sessionId, cwd)
108
- if (!existing) return null
109
- return this.update(sessionId, { stopRequested: true }, cwd)
110
- },
111
- async clearStop(sessionId, cwd = process.cwd()) {
112
- const existing = await this.get(sessionId, cwd)
113
- if (!existing) return null
114
- return this.update(sessionId, { stopRequested: false }, cwd)
115
- }
116
- }
1
+ import path from "node:path"
2
+ import { mkdir, writeFile, readFile, unlink, stat } from "node:fs/promises"
3
+ import { readJson, writeJson } from "../storage/json-store.mjs"
4
+ import { projectRootDir } from "../storage/paths.mjs"
5
+
6
+ function statePath(cwd = process.cwd()) {
7
+ return path.join(projectRootDir(cwd), "longagent-state.json")
8
+ }
9
+
10
+ function lockPath(cwd = process.cwd()) {
11
+ return statePath(cwd) + ".lock"
12
+ }
13
+
14
+ async function ensure(cwd = process.cwd()) {
15
+ await mkdir(projectRootDir(cwd), { recursive: true })
16
+ }
17
+
18
+ const LOCK_TIMEOUT_MS = 5000
19
+ const LOCK_STALE_MS = LOCK_TIMEOUT_MS * 0.8 // 4000ms — detect stale before timeout
20
+ const LOCK_RETRY_INIT_MS = 50
21
+ const LOCK_RETRY_MAX_MS = 500
22
+
23
+ function isProcessAlive(pid) {
24
+ try {
25
+ process.kill(pid, 0)
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ async function tryRemoveStaleLock(file, staleMs = LOCK_STALE_MS) {
33
+ try {
34
+ const content = await readFile(file, "utf-8")
35
+ const [pidStr] = content.split(":")
36
+ const pid = Number(pidStr)
37
+ // If PID is valid and process is dead, remove immediately
38
+ if (pid > 0 && !isProcessAlive(pid)) {
39
+ await unlink(file).catch(() => {})
40
+ return true
41
+ }
42
+ // Otherwise check mtime-based staleness
43
+ const info = await stat(file)
44
+ if (Date.now() - info.mtimeMs > staleMs) {
45
+ await unlink(file).catch(() => {})
46
+ return true
47
+ }
48
+ } catch {
49
+ // lock disappeared or unreadable — retry
50
+ return true
51
+ }
52
+ return false
53
+ }
54
+
55
+ async function acquireLock(cwd, lockTimeoutMs = LOCK_TIMEOUT_MS) {
56
+ await ensure(cwd)
57
+ const file = lockPath(cwd)
58
+ const staleMs = lockTimeoutMs * 0.8
59
+ const deadline = Date.now() + lockTimeoutMs
60
+ let retryMs = LOCK_RETRY_INIT_MS
61
+
62
+ while (Date.now() < deadline) {
63
+ try {
64
+ await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
65
+ return true
66
+ } catch (err) {
67
+ if (err.code !== "EEXIST") throw err
68
+ const removed = await tryRemoveStaleLock(file, staleMs)
69
+ if (removed) continue
70
+ // Exponential backoff: 50 → 100 → 200 → 400 → 500 (capped)
71
+ await new Promise((r) => setTimeout(r, retryMs))
72
+ retryMs = Math.min(retryMs * 2, LOCK_RETRY_MAX_MS)
73
+ }
74
+ }
75
+
76
+ // Final attempt after timeout
77
+ const removed = await tryRemoveStaleLock(file, staleMs)
78
+ if (removed) {
79
+ try {
80
+ await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
81
+ return true
82
+ } catch { /* another process grabbed it */ }
83
+ }
84
+ throw new Error(`Failed to acquire lock after ${lockTimeoutMs}ms: ${file}`)
85
+ }
86
+
87
+ async function releaseLock(cwd) {
88
+ await unlink(lockPath(cwd)).catch(() => {})
89
+ }
90
+
91
+ async function read(cwd = process.cwd()) {
92
+ await ensure(cwd)
93
+ return readJson(statePath(cwd), { sessions: {} })
94
+ }
95
+
96
+ async function write(data, cwd = process.cwd()) {
97
+ await ensure(cwd)
98
+ await writeJson(statePath(cwd), data)
99
+ }
100
+
101
+ export const LongAgentManager = {
102
+ async update(sessionId, patch, cwd = process.cwd(), config = null) {
103
+ const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
104
+ await acquireLock(cwd, lockMs)
105
+ try {
106
+ const state = await read(cwd)
107
+ const current = state.sessions[sessionId] || {
108
+ sessionId,
109
+ status: "idle",
110
+ phase: "L0",
111
+ gateStatus: {},
112
+ currentGate: "execution",
113
+ recoveryCount: 0,
114
+ planFrozen: false,
115
+ currentStageId: null,
116
+ stageIndex: 0,
117
+ stageCount: 0,
118
+ stageStatus: null,
119
+ taskProgress: {},
120
+ remainingFiles: [],
121
+ remainingFilesCount: 0,
122
+ lastGateFailures: [],
123
+ createdAt: Date.now(),
124
+ updatedAt: Date.now(),
125
+ heartbeatAt: null,
126
+ iterations: 0,
127
+ lastMessage: ""
128
+ }
129
+ state.sessions[sessionId] = {
130
+ ...current,
131
+ ...patch,
132
+ updatedAt: Date.now()
133
+ }
134
+ await write(state, cwd)
135
+ return state.sessions[sessionId]
136
+ } finally {
137
+ await releaseLock(cwd)
138
+ }
139
+ },
140
+ async get(sessionId, cwd = process.cwd()) {
141
+ const state = await read(cwd)
142
+ return state.sessions[sessionId] || null
143
+ },
144
+ async list(cwd = process.cwd()) {
145
+ const state = await read(cwd)
146
+ return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
147
+ },
148
+ async stop(sessionId, cwd = process.cwd()) {
149
+ const existing = await this.get(sessionId, cwd)
150
+ if (!existing) return null
151
+ return this.update(sessionId, { stopRequested: true }, cwd)
152
+ },
153
+ async clearStop(sessionId, cwd = process.cwd()) {
154
+ const existing = await this.get(sessionId, cwd)
155
+ if (!existing) return null
156
+ return this.update(sessionId, { stopRequested: false }, cwd)
157
+ },
158
+ /**
159
+ * Execute `fn` while holding the state lock.
160
+ * Prevents TOCTOU races (e.g. read-status → git-merge).
161
+ */
162
+ async withLock(fn, cwd = process.cwd(), config = null) {
163
+ const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
164
+ await acquireLock(cwd, lockMs)
165
+ try {
166
+ return await fn()
167
+ } finally {
168
+ await releaseLock(cwd)
169
+ }
170
+ }
171
+ }