@cmodjs/core 0.3.0 → 1.5.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/docs/commands.md +61 -10
- package/docs/dependencies.md +16 -14
- package/docs/hooks.md +64 -29
- package/docs/index.md +29 -25
- package/docs/install-steps.md +13 -9
- package/docs/jobs.md +59 -111
- package/docs/mod.md +227 -175
- package/docs/options.md +2 -2
- package/docs/permissions.md +27 -16
- package/docs/session.md +311 -0
- package/docs/state.md +11 -7
- package/docs/testing.md +68 -23
- package/docs/ui.md +116 -76
- package/jobs/check.d.ts.map +1 -1
- package/jobs/check.js +5 -2
- package/jobs/context.d.ts +7 -8
- package/jobs/context.d.ts.map +1 -1
- package/jobs/context.js +23 -73
- package/jobs/permissions/decide-permission.d.ts +5 -3
- package/jobs/permissions/decide-permission.d.ts.map +1 -1
- package/jobs/permissions/decide-permission.js +6 -4
- package/jobs/permissions/find-project-scope.d.ts +1 -1
- package/jobs/permissions/find-project-scope.d.ts.map +1 -1
- package/jobs/permissions/find-project-scope.js +8 -11
- package/jobs/permissions/match-target.d.ts +2 -2
- package/jobs/permissions/match-target.d.ts.map +1 -1
- package/jobs/permissions.d.ts +1 -1
- package/jobs/permissions.d.ts.map +1 -1
- package/jobs/permissions.js +6 -4
- package/jobs/program.d.ts.map +1 -1
- package/jobs/program.js +14 -4
- package/jobs/prompt.d.ts +4 -2
- package/jobs/prompt.d.ts.map +1 -1
- package/jobs/prompt.js +16 -15
- package/jobs/schedule.d.ts +2 -2
- package/jobs/schedule.d.ts.map +1 -1
- package/jobs/schedule.js +9 -9
- package/jobs/slash-command.d.ts +4 -4
- package/jobs/slash-command.d.ts.map +1 -1
- package/jobs/slash-command.js +14 -9
- package/jobs/status-line.d.ts +1 -11
- package/jobs/status-line.d.ts.map +1 -1
- package/jobs/status-line.js +8 -12
- package/jobs/tool.d.ts +2 -2
- package/jobs/tool.d.ts.map +1 -1
- package/jobs/tool.js +15 -9
- package/mod.d.ts +178 -75
- package/mod.d.ts.map +1 -1
- package/mod.js +127 -3
- package/options.d.ts +2 -2
- package/options.js +19 -19
- package/package.json +3 -3
- package/records.d.ts +13 -8
- package/records.d.ts.map +1 -1
- package/records.js +33 -17
- package/register.d.ts.map +1 -1
- package/register.js +34 -5
- package/runtime/announce.d.ts +18 -0
- package/runtime/announce.d.ts.map +1 -0
- package/runtime/announce.js +68 -0
- package/runtime/before.d.ts +32 -0
- package/runtime/before.d.ts.map +1 -0
- package/runtime/before.js +214 -0
- package/runtime/bound.d.ts +4 -0
- package/runtime/bound.d.ts.map +1 -0
- package/runtime/bound.js +9 -0
- package/runtime/claude.d.ts +4 -2
- package/runtime/claude.d.ts.map +1 -1
- package/runtime/conversations.d.ts +17 -0
- package/runtime/conversations.d.ts.map +1 -0
- package/runtime/conversations.js +563 -0
- package/runtime/deadline.d.ts +0 -1
- package/runtime/deadline.d.ts.map +1 -1
- package/runtime/deadline.js +1 -1
- package/runtime/dependencies.d.ts +2 -0
- package/runtime/dependencies.d.ts.map +1 -1
- package/runtime/dependencies.js +10 -1
- package/runtime/engine.d.ts +13 -0
- package/runtime/engine.d.ts.map +1 -0
- package/runtime/engine.js +10 -0
- package/runtime/grants.d.ts +4 -0
- package/runtime/grants.d.ts.map +1 -1
- package/runtime/grants.js +68 -31
- package/runtime/hooks.d.ts +5 -4
- package/runtime/hooks.d.ts.map +1 -1
- package/runtime/hooks.js +29 -9
- package/runtime/installer.d.ts +1 -1
- package/runtime/installer.d.ts.map +1 -1
- package/runtime/installer.js +17 -14
- package/runtime/lifecycle.d.ts +3 -1
- package/runtime/lifecycle.d.ts.map +1 -1
- package/runtime/lifecycle.js +414 -212
- package/runtime/metadata.d.ts +3 -1
- package/runtime/metadata.d.ts.map +1 -1
- package/runtime/metadata.js +25 -14
- package/runtime/options.d.ts +3 -1
- package/runtime/options.d.ts.map +1 -1
- package/runtime/options.js +4 -4
- package/runtime/programs.d.ts +8 -0
- package/runtime/programs.d.ts.map +1 -1
- package/runtime/programs.js +39 -1
- package/runtime/router.d.ts +2 -1
- package/runtime/router.d.ts.map +1 -1
- package/runtime/router.js +11 -3
- package/runtime/sessions.d.ts +58 -0
- package/runtime/sessions.d.ts.map +1 -0
- package/runtime/sessions.js +433 -0
- package/runtime/state.d.ts +6 -2
- package/runtime/state.d.ts.map +1 -1
- package/runtime/state.js +46 -13
- package/runtime/toaster.d.ts +24 -0
- package/runtime/toaster.d.ts.map +1 -0
- package/runtime/toaster.js +286 -0
- package/runtime/ui.d.ts +6 -17
- package/runtime/ui.d.ts.map +1 -1
- package/runtime/ui.js +62 -118
- package/session/context/context.d.ts +12 -0
- package/session/context/context.d.ts.map +1 -0
- package/session/context/context.js +2 -0
- package/session/context/turn/message/document.d.ts +7 -0
- package/session/context/turn/message/document.d.ts.map +1 -0
- package/session/context/turn/message/document.js +1 -0
- package/session/context/turn/message/message.d.ts +55 -0
- package/session/context/turn/message/message.d.ts.map +1 -0
- package/session/context/turn/message/message.js +178 -0
- package/session/context/turn/message/note.d.ts +8 -0
- package/session/context/turn/message/note.d.ts.map +1 -0
- package/session/context/turn/message/note.js +1 -0
- package/session/context/turn/message/notice.d.ts +6 -0
- package/session/context/turn/message/notice.d.ts.map +1 -0
- package/session/context/turn/message/notice.js +1 -0
- package/session/context/turn/message/prompt.d.ts +8 -0
- package/session/context/turn/message/prompt.d.ts.map +1 -0
- package/session/context/turn/message/prompt.js +1 -0
- package/session/context/turn/message/reply.d.ts +6 -0
- package/session/context/turn/message/reply.d.ts.map +1 -0
- package/session/context/turn/message/reply.js +1 -0
- package/session/context/turn/message/sender.d.ts +14 -0
- package/session/context/turn/message/sender.d.ts.map +1 -0
- package/session/context/turn/message/sender.js +30 -0
- package/session/context/turn/message/skill.d.ts +7 -0
- package/session/context/turn/message/skill.d.ts.map +1 -0
- package/session/context/turn/message/skill.js +1 -0
- package/session/context/turn/message/tool-call.d.ts +9 -0
- package/session/context/turn/message/tool-call.d.ts.map +1 -0
- package/session/context/turn/message/tool-call.js +1 -0
- package/session/context/turn/message/tool-result.d.ts +8 -0
- package/session/context/turn/message/tool-result.d.ts.map +1 -0
- package/session/context/turn/message/tool-result.js +1 -0
- package/session/context/turn/turn.d.ts +13 -0
- package/session/context/turn/turn.d.ts.map +1 -0
- package/session/context/turn/turn.js +2 -0
- package/session/conversation.d.ts +73 -0
- package/session/conversation.d.ts.map +1 -0
- package/session/conversation.js +146 -0
- package/session/goal/goal.d.ts +11 -0
- package/session/goal/goal.d.ts.map +1 -0
- package/session/goal/goal.js +1 -0
- package/session/goal/task.d.ts +16 -0
- package/session/goal/task.d.ts.map +1 -0
- package/session/goal/task.js +3 -0
- package/session/session.d.ts +158 -0
- package/session/session.d.ts.map +1 -0
- package/session/session.js +2 -0
- package/src/jobs/check.ts +7 -4
- package/src/jobs/context.ts +24 -82
- package/src/jobs/permissions/decide-permission.ts +10 -8
- package/src/jobs/permissions/find-project-scope.ts +8 -14
- package/src/jobs/permissions/match-target.ts +3 -3
- package/src/jobs/permissions.ts +8 -6
- package/src/jobs/program.ts +13 -4
- package/src/jobs/prompt.ts +21 -24
- package/src/jobs/schedule.ts +14 -14
- package/src/jobs/slash-command.ts +20 -14
- package/src/jobs/status-line.ts +9 -21
- package/src/jobs/tool.ts +17 -11
- package/src/mod.ts +252 -97
- package/src/options.ts +22 -22
- package/src/records.ts +40 -23
- package/src/register.ts +34 -5
- package/src/runtime/announce.ts +76 -0
- package/src/runtime/before.ts +200 -0
- package/src/runtime/bound.ts +12 -0
- package/src/runtime/claude.ts +7 -2
- package/src/runtime/conversations.ts +579 -0
- package/src/runtime/deadline.ts +2 -2
- package/src/runtime/dependencies.ts +13 -1
- package/src/runtime/engine.ts +23 -0
- package/src/runtime/grants.ts +69 -29
- package/src/runtime/hooks.ts +39 -14
- package/src/runtime/installer.ts +16 -14
- package/src/runtime/lifecycle.ts +413 -201
- package/src/runtime/metadata.ts +37 -14
- package/src/runtime/options.ts +19 -4
- package/src/runtime/programs.ts +40 -1
- package/src/runtime/router.ts +11 -4
- package/src/runtime/sessions.ts +447 -0
- package/src/runtime/state.ts +50 -14
- package/src/runtime/toaster.ts +334 -0
- package/src/runtime/ui.ts +65 -143
- package/src/session/context/context.ts +14 -0
- package/src/session/context/turn/message/document.ts +8 -0
- package/src/session/context/turn/message/message.ts +203 -0
- package/src/session/context/turn/message/note.ts +9 -0
- package/src/session/context/turn/message/notice.ts +7 -0
- package/src/session/context/turn/message/prompt.ts +9 -0
- package/src/session/context/turn/message/reply.ts +7 -0
- package/src/session/context/turn/message/sender.ts +38 -0
- package/src/session/context/turn/message/skill.ts +8 -0
- package/src/session/context/turn/message/tool-call.ts +10 -0
- package/src/session/context/turn/message/tool-result.ts +9 -0
- package/src/session/context/turn/turn.ts +15 -0
- package/src/session/conversation.ts +194 -0
- package/src/session/goal/goal.ts +10 -0
- package/src/session/goal/task.ts +19 -0
- package/src/session/session.ts +141 -0
- package/src/testing/fake-claude.ts +25 -17
- package/src/testing/fake-elements.ts +3 -2
- package/src/testing/fake-files.ts +1 -1
- package/src/testing/fake-sessions.ts +277 -0
- package/src/testing.ts +138 -35
- package/src/ui/components.ts +3 -3
- package/src/ui/elements.ts +22 -8
- package/src/ui/markdown.ts +22 -26
- package/src/ui/places.ts +49 -0
- package/src/utils/call-effects.ts +5 -5
- package/src/utils/changelog.ts +32 -0
- package/src/utils/disposable.ts +3 -0
- package/src/utils/matcher.ts +37 -0
- package/src/utils/metadata.ts +17 -15
- package/src/utils/parse-shell.ts +9 -8
- package/src/utils/paths.ts +21 -4
- package/src/utils/text.ts +4 -0
- package/testing/fake-claude.d.ts +5 -1
- package/testing/fake-claude.d.ts.map +1 -1
- package/testing/fake-claude.js +21 -12
- package/testing/fake-elements.js +4 -2
- package/testing/fake-files.js +1 -1
- package/testing/fake-sessions.d.ts +81 -0
- package/testing/fake-sessions.d.ts.map +1 -0
- package/testing/fake-sessions.js +249 -0
- package/testing.d.ts +34 -13
- package/testing.d.ts.map +1 -1
- package/testing.js +115 -23
- package/ui/components.d.ts +2 -3
- package/ui/components.d.ts.map +1 -1
- package/ui/components.js +3 -3
- package/ui/elements.d.ts.map +1 -1
- package/ui/elements.js +19 -9
- package/ui/markdown.d.ts +35 -33
- package/ui/markdown.d.ts.map +1 -1
- package/ui/markdown.js +14 -16
- package/ui/places.d.ts +43 -0
- package/ui/places.d.ts.map +1 -0
- package/ui/places.js +31 -0
- package/utils/call-effects.d.ts +2 -1
- package/utils/call-effects.d.ts.map +1 -1
- package/utils/call-effects.js +3 -3
- package/utils/changelog.d.ts +9 -0
- package/utils/changelog.d.ts.map +1 -0
- package/utils/changelog.js +29 -0
- package/utils/disposable.d.ts +4 -0
- package/utils/disposable.d.ts.map +1 -0
- package/utils/disposable.js +1 -0
- package/utils/matcher.d.ts +13 -0
- package/utils/matcher.d.ts.map +1 -0
- package/utils/matcher.js +32 -0
- package/utils/metadata.d.ts +3 -3
- package/utils/metadata.js +18 -14
- package/utils/parse-shell.js +9 -7
- package/utils/paths.d.ts +4 -1
- package/utils/paths.d.ts.map +1 -1
- package/utils/paths.js +19 -4
- package/utils/text.d.ts +1 -0
- package/utils/text.d.ts.map +1 -1
- package/utils/text.js +3 -0
- package/src/ui/slots.ts +0 -36
- package/ui/slots.d.ts +0 -208
- package/ui/slots.d.ts.map +0 -1
- package/ui/slots.js +0 -19
package/src/runtime/metadata.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { FileMatch, Metadata } from '../mod.js'
|
|
2
2
|
import { formatOf, MetadataError, readMetadata, sidecarSuffix, writeMetadata, type MetadataFormat } from '../utils/metadata.js'
|
|
3
|
+
import { expandHome, homePrefix, ifMissing, isMissing } from '../utils/paths.js'
|
|
3
4
|
import { messageOf } from '../utils/text.js'
|
|
4
5
|
import { picomatch, resolve } from '../vendor.js'
|
|
5
6
|
import type { Claude } from './claude.js'
|
|
7
|
+
import type { Failure } from './toaster.js'
|
|
6
8
|
|
|
7
9
|
export type Places = {
|
|
8
10
|
readonly home: string | undefined
|
|
@@ -27,14 +29,27 @@ const ownKey = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/
|
|
|
27
29
|
export function resolvedPath(path: string, places: Places): string {
|
|
28
30
|
const configured = /^~\/\.claude(?=\/|$)/.exec(path)
|
|
29
31
|
if (configured !== null && places.configRoot !== undefined) return resolve(`${places.configRoot}${path.slice(configured[0].length)}`)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const prefix = homePrefix.exec(path)?.[0]
|
|
33
|
+
if (prefix !== undefined) {
|
|
34
|
+
if (places.home === undefined) throw new Error(`${path} starts with ${prefix}, and HOME is not set. Start Claude Code with HOME set.`)
|
|
35
|
+
return resolve(expandHome(path, places.home, places.configRoot))
|
|
33
36
|
}
|
|
34
37
|
return path.startsWith('/') ? resolve(path) : resolve(places.projectRoot(), path)
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export function createModFiles({
|
|
40
|
+
export function createModFiles({
|
|
41
|
+
name,
|
|
42
|
+
claude,
|
|
43
|
+
places,
|
|
44
|
+
checkWrite,
|
|
45
|
+
report,
|
|
46
|
+
}: {
|
|
47
|
+
readonly name: string
|
|
48
|
+
readonly claude: Claude
|
|
49
|
+
readonly places: Places
|
|
50
|
+
readonly checkWrite: (path: string) => Promise<void>
|
|
51
|
+
readonly report: (failure: Failure) => void
|
|
52
|
+
}): ModFiles {
|
|
38
53
|
const prefix = `${name}.`
|
|
39
54
|
const cache = new Map<string, { readonly signature: string; readonly read: Read }>()
|
|
40
55
|
const logged = new Set<string>()
|
|
@@ -48,12 +63,20 @@ export function createModFiles({ name, claude, places, checkWrite }: { readonly
|
|
|
48
63
|
const extensionless = !realPath.slice(realPath.lastIndexOf('/') + 1).includes('.')
|
|
49
64
|
const firstLine = extensionless ? (await claude.fs.read(realPath)).split('\n', 1)[0] : undefined
|
|
50
65
|
const format = formatOf(realPath, firstLine)
|
|
51
|
-
const holder = format.
|
|
52
|
-
const sidecar = format.
|
|
66
|
+
const holder = format.type === 'sidecar' ? `${realPath}${sidecarSuffix}` : realPath
|
|
67
|
+
const sidecar = format.type === 'sidecar' ? await claude.fs.stat(holder).catch(ifMissing(undefined)) : undefined
|
|
53
68
|
return { realPath, format, holder, signature: `${stat.mtimeMs}:${stat.size}:${sidecar?.kind === 'file' ? `${sidecar.mtimeMs}:${sidecar.size}` : ''}` }
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
const textOf = async (holder: string): Promise<string> => ((await claude.fs.stat(holder).catch((
|
|
71
|
+
const textOf = async (holder: string): Promise<string> => ((await claude.fs.stat(holder).catch(ifMissing(undefined)))?.kind === 'file' ? claude.fs.read(holder) : '')
|
|
72
|
+
|
|
73
|
+
const skipped = <Value>(path: string, value: Value) => (error: unknown): Value => {
|
|
74
|
+
if (isMissing(error)) return value
|
|
75
|
+
const reason = `${name}: fs.find skipped ${path}: ${messageOf(error)}`
|
|
76
|
+
if (!logged.has(reason)) claude.ui.log(reason, { to: 'debug' })
|
|
77
|
+
logged.add(reason)
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
57
80
|
|
|
58
81
|
const readEntry = async (path: string): Promise<Read> => {
|
|
59
82
|
const located = await locate(path)
|
|
@@ -67,7 +90,7 @@ export function createModFiles({ name, claude, places, checkWrite }: { readonly
|
|
|
67
90
|
if (!(error instanceof MetadataError)) throw error
|
|
68
91
|
const failure = `${located.holder} ${error.message}`
|
|
69
92
|
read = { metadata: {}, error: failure }
|
|
70
|
-
if (!logged.has(failure))
|
|
93
|
+
if (!logged.has(failure)) report({ id: `metadata:${located.holder}`, message: `Skips the metadata of ${located.holder}`, description: failure })
|
|
71
94
|
logged.add(failure)
|
|
72
95
|
}
|
|
73
96
|
cache.set(path, { signature: located.signature, read })
|
|
@@ -77,21 +100,21 @@ export function createModFiles({ name, claude, places, checkWrite }: { readonly
|
|
|
77
100
|
const matchesOf = async (pattern: string): Promise<string[]> => {
|
|
78
101
|
const absolute = resolvedPath(pattern, places)
|
|
79
102
|
const scanned = picomatch.scan(absolute)
|
|
80
|
-
if (!scanned.isGlob) return (await claude.fs.stat(absolute).catch((
|
|
103
|
+
if (!scanned.isGlob) return (await claude.fs.stat(absolute).catch(skipped(absolute, undefined)))?.kind === 'file' ? [absolute] : []
|
|
81
104
|
const isMatch = picomatch(absolute, { dot: true })
|
|
82
105
|
const entersSkipped = skippedFolders.filter((folder) => scanned.glob.split('/').includes(folder))
|
|
83
106
|
const maxDepth = scanned.glob.includes('**') ? Number.POSITIVE_INFINITY : scanned.glob.split('/').length
|
|
84
107
|
const found: string[] = []
|
|
85
108
|
const visited = new Set<string>()
|
|
86
109
|
const walk = async (folder: string, depth: number): Promise<void> => {
|
|
87
|
-
const real = (await claude.fs.stat(folder, { resolve: true }).catch((
|
|
110
|
+
const real = (await claude.fs.stat(folder, { resolve: true }).catch(skipped(folder, undefined)))?.realPath ?? folder
|
|
88
111
|
if (visited.has(real)) return
|
|
89
112
|
visited.add(real)
|
|
90
|
-
const entries = await claude.fs.list(folder).catch((
|
|
113
|
+
const entries = await claude.fs.list(folder).catch(skipped(folder, []))
|
|
91
114
|
await Promise.all(
|
|
92
115
|
entries.map(async (entry) => {
|
|
93
116
|
const path = `${folder}/${entry.name}`
|
|
94
|
-
const kind = entry.isLink === true ? (await claude.fs.stat(path).catch((
|
|
117
|
+
const kind = entry.isLink === true ? (await claude.fs.stat(path).catch(skipped(path, undefined)))?.kind : entry.kind
|
|
95
118
|
if (kind === 'file' && isMatch(path)) found.push(path)
|
|
96
119
|
if (kind !== 'dir' || depth + 1 >= maxDepth) return
|
|
97
120
|
if (skippedFolders.includes(entry.name) && !entersSkipped.includes(entry.name)) return
|
|
@@ -100,7 +123,7 @@ export function createModFiles({ name, claude, places, checkWrite }: { readonly
|
|
|
100
123
|
)
|
|
101
124
|
}
|
|
102
125
|
const base = scanned.base === '' ? '/' : scanned.base
|
|
103
|
-
if ((await claude.fs.stat(base).catch((
|
|
126
|
+
if ((await claude.fs.stat(base).catch(skipped(base, undefined)))?.kind === 'dir') await walk(base, 0)
|
|
104
127
|
return found
|
|
105
128
|
}
|
|
106
129
|
|
|
@@ -111,7 +134,7 @@ export function createModFiles({ name, claude, places, checkWrite }: { readonly
|
|
|
111
134
|
const kept = paths.filter((path) => !(path.endsWith(sidecarSuffix) && paths.includes(path.slice(0, -sidecarSuffix.length))))
|
|
112
135
|
return Promise.all(
|
|
113
136
|
kept.map(async (path): Promise<FileMatch> => {
|
|
114
|
-
const read = await readEntry(path)
|
|
137
|
+
const read = await readEntry(path).catch((error: unknown): Read => ({ metadata: {}, error: `${path} ${messageOf(error)}` }))
|
|
115
138
|
return { path, ...(read.name === undefined ? {} : { name: read.name }), metadata: own(read.metadata), ...(read.error === undefined ? {} : { error: read.error }) }
|
|
116
139
|
}),
|
|
117
140
|
)
|
package/src/runtime/options.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { fitsOption, isUnset, type Option, type Options } from '../options.js'
|
|
|
3
3
|
import { configFolders } from '../records.js'
|
|
4
4
|
import { listed, messageOf } from '../utils/text.js'
|
|
5
5
|
import type { Claude } from './claude.js'
|
|
6
|
+
import type { Failure } from './toaster.js'
|
|
6
7
|
|
|
7
8
|
export class MissingOptions extends Error {
|
|
8
9
|
readonly keys: readonly string[]
|
|
@@ -21,7 +22,21 @@ export type ModOptions = {
|
|
|
21
22
|
load(root: string): Promise<void>
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function createOptions({
|
|
25
|
+
export function createOptions({
|
|
26
|
+
name,
|
|
27
|
+
declared,
|
|
28
|
+
fromClaude,
|
|
29
|
+
claude,
|
|
30
|
+
report,
|
|
31
|
+
changed,
|
|
32
|
+
}: {
|
|
33
|
+
readonly name: string
|
|
34
|
+
readonly declared: Options
|
|
35
|
+
readonly fromClaude: PluginOptions
|
|
36
|
+
readonly claude: Claude
|
|
37
|
+
readonly report: (failure: Failure) => void
|
|
38
|
+
readonly changed: () => void
|
|
39
|
+
}): ModOptions {
|
|
25
40
|
let values: Readonly<Record<string, unknown>> = merged(declared, fromClaude, {}, new Set())
|
|
26
41
|
let locked: Set<string> | undefined
|
|
27
42
|
let loadedRoot: string | undefined
|
|
@@ -41,7 +56,7 @@ export function createOptions({ name, declared, fromClaude, claude, changed }: {
|
|
|
41
56
|
if (project === undefined) return {}
|
|
42
57
|
const path = `${project.folder}/options.json`
|
|
43
58
|
if (!(await claude.fs.exists(path))) return {}
|
|
44
|
-
const ignore = (reason: string) =>
|
|
59
|
+
const ignore = (reason: string) => report({ id: `options:${path} ${reason}`, message: `Ignores part of ${path}`, description: `${path} ${reason}` })
|
|
45
60
|
let file: unknown
|
|
46
61
|
try {
|
|
47
62
|
file = JSON.parse(await claude.fs.read(path))
|
|
@@ -57,7 +72,7 @@ export function createOptions({ name, declared, fromClaude, claude, changed }: {
|
|
|
57
72
|
for (const [key, value] of Object.entries(file)) {
|
|
58
73
|
const option = declared[key]
|
|
59
74
|
if (option === undefined) ignore(`sets ${key}, which ${name} does not declare. ${Object.keys(declared).length === 0 ? 'Remove it.' : `Remove it, or use one of: ${Object.keys(declared).join(', ')}.`}`)
|
|
60
|
-
else if (option.
|
|
75
|
+
else if (option.type === 'secret') ignore(`sets ${key}, a secret, and a project file is committed for everyone to read. Remove it, and set it in /config.`)
|
|
61
76
|
else {
|
|
62
77
|
const misfit = fitsOption(option, value)
|
|
63
78
|
if (misfit === undefined) kept[key] = value
|
|
@@ -98,5 +113,5 @@ function valueOf(key: string, option: Option, own: unknown, project: Readonly<Re
|
|
|
98
113
|
if (locked.has(key)) return own
|
|
99
114
|
if (Object.hasOwn(project, key)) return project[key]
|
|
100
115
|
if (!isUnset(own) && fitsOption(option, own) === undefined) return Array.isArray(own) ? Object.freeze([...own]) : own
|
|
101
|
-
return option.default ?? (option.
|
|
116
|
+
return option.default ?? (option.type === 'list' ? Object.freeze([]) : option.type === 'number' ? undefined : '')
|
|
102
117
|
}
|
package/src/runtime/programs.ts
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
+
import type { HookStream, ProcessSpawnChunk, ProcessSpawnResult } from 'claude-code'
|
|
1
2
|
import { storeFolder } from '../records.js'
|
|
3
|
+
import { ifMissing } from '../utils/paths.js'
|
|
4
|
+
import { formatExit } from '../utils/text.js'
|
|
2
5
|
import type { Claude } from './claude.js'
|
|
3
6
|
|
|
4
7
|
export type Programs = { readonly claude: Claude; refresh(): Promise<void> }
|
|
5
8
|
|
|
9
|
+
export function lastLineOf(text: string): string | undefined {
|
|
10
|
+
const trimmed = text.trim()
|
|
11
|
+
return trimmed === '' ? undefined : trimmed.slice(trimmed.lastIndexOf('\n') + 1).trim()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function cmodError(stderr: string, command: string | undefined): string {
|
|
15
|
+
const line = lastLineOf(stderr) ?? ''
|
|
16
|
+
const prefix = `cmod ${command}: `
|
|
17
|
+
return line.startsWith(prefix) ? line.slice(prefix.length) : line
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readLines(stream: HookStream<ProcessSpawnChunk, ProcessSpawnResult>, onLine: (text: string) => void): Promise<{ code: number | null; lastError: string }> {
|
|
21
|
+
let pending = ''
|
|
22
|
+
let lastError = ''
|
|
23
|
+
let piece = await stream.next()
|
|
24
|
+
while (piece.done !== true) {
|
|
25
|
+
if (piece.value.stream === 'stderr') {
|
|
26
|
+
lastError = lastLineOf(piece.value.text) ?? lastError
|
|
27
|
+
} else {
|
|
28
|
+
const lines = `${pending}${piece.value.text}`.split('\n')
|
|
29
|
+
pending = lines.pop() ?? ''
|
|
30
|
+
for (const text of lines) onLine(text)
|
|
31
|
+
}
|
|
32
|
+
piece = await stream.next()
|
|
33
|
+
}
|
|
34
|
+
if (pending !== '') onLine(pending)
|
|
35
|
+
return { code: piece.value.code, lastError }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runCmod(claude: Claude, args: readonly string[], session: string): Promise<string> {
|
|
39
|
+
const lines: string[] = []
|
|
40
|
+
const { code, lastError } = await readLines(claude.process.spawn({ argv: ['cmod', ...args], env: { CLAUDE_CODE_SESSION_ID: session } }), (line) => void lines.push(line))
|
|
41
|
+
if (code !== 0) throw new Error(cmodError(lastError, args[0]) || `cmod ${args.slice(0, 2).join(' ')} ${formatExit(code, '')}.`)
|
|
42
|
+
return lines.join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
6
45
|
export function locatingPrograms(claude: Claude): Programs {
|
|
7
46
|
let folder: string | undefined
|
|
8
47
|
let names: ReadonlySet<string> = new Set()
|
|
@@ -21,7 +60,7 @@ export function locatingPrograms(claude: Claude): Programs {
|
|
|
21
60
|
async refresh() {
|
|
22
61
|
const [home, dataHome] = await Promise.all([claude.env.home(), claude.env.dataHome()])
|
|
23
62
|
const programs = `${storeFolder({ HOME: home, XDG_DATA_HOME: dataHome })}/programs`
|
|
24
|
-
names = new Set((await claude.fs.list(programs).catch((
|
|
63
|
+
names = new Set((await claude.fs.list(programs).catch(ifMissing([]))).map((entry) => entry.name))
|
|
25
64
|
folder = programs
|
|
26
65
|
},
|
|
27
66
|
}
|
package/src/runtime/router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Args, EventResult, Frozen } from 'claude-code'
|
|
2
|
+
import type { Disposable } from '../mod.js'
|
|
2
3
|
import type { RoutedEvent } from './hooks.js'
|
|
3
4
|
|
|
4
5
|
export type RouterNext<N extends RoutedEvent> = (e: Frozen<Args<N>>) => Promise<EventResult<N>>
|
|
@@ -6,7 +7,7 @@ export type RouterNext<N extends RoutedEvent> = (e: Frozen<Args<N>>) => Promise<
|
|
|
6
7
|
export type RoutedHook<N extends RoutedEvent> = (e: Frozen<Args<N>>, next: RouterNext<N>) => EventResult<N> | Promise<EventResult<N>>
|
|
7
8
|
|
|
8
9
|
export type Router = {
|
|
9
|
-
add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>):
|
|
10
|
+
add<N extends RoutedEvent>(event: N, hook: RoutedHook<N>): Disposable
|
|
10
11
|
has(event: RoutedEvent): boolean
|
|
11
12
|
dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>>
|
|
12
13
|
}
|
|
@@ -18,9 +19,15 @@ export function createRouter(): Router {
|
|
|
18
19
|
|
|
19
20
|
return {
|
|
20
21
|
add(event, hook) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const erased = hook as unknown as ErasedHook
|
|
23
|
+
hooks.set(event, [...(hooks.get(event) ?? []), erased])
|
|
24
|
+
return {
|
|
25
|
+
dispose() {
|
|
26
|
+
const chain = (hooks.get(event) ?? []).filter((each) => each !== erased)
|
|
27
|
+
if (chain.length === 0) hooks.delete(event)
|
|
28
|
+
else hooks.set(event, chain)
|
|
29
|
+
},
|
|
30
|
+
}
|
|
24
31
|
},
|
|
25
32
|
has: (event) => hooks.has(event),
|
|
26
33
|
dispatch<N extends RoutedEvent>(event: N, e: Frozen<Args<N>>, next: RouterNext<N>): Promise<EventResult<N>> {
|