rvlib-shipkit 2.2.0 → 2.4.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/CHANGELOG.md +47 -0
- package/package.json +1 -1
- package/src/agent-time/mask.ts +43 -0
- package/src/agent-time/render.ts +187 -0
- package/src/agent-time/report.ts +335 -0
- package/src/agent-time/shape.ts +271 -0
- package/src/agent-time/signals.ts +84 -0
- package/src/agent-time/store.ts +379 -0
- package/src/agent-time/timeline.ts +218 -0
- package/src/agent-time/turns.ts +135 -0
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +9 -0
- package/src/cli/cmd_Review.ts +974 -0
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Time.ts +112 -0
- package/src/cli/cmd_Trailer.ts +264 -6
- package/src/cli/kit/verb.ts +22 -4
- package/src/cli.ts +8 -4
- package/src/collections/collection-types.ts +6 -1
- package/src/collections/providers.ts +3 -0
- package/src/collections/review.ts +70 -0
- package/src/core/app-icon.ts +10 -6
- package/src/core/layout.ts +2 -0
- package/src/reflect/prompt-corpus.ts +15 -119
- package/src/reflect/transcript.ts +254 -0
- package/src/repo-config.ts +3 -0
- package/src/review/anim.ts +228 -0
- package/src/review/auto.ts +141 -0
- package/src/review/format.ts +158 -0
- package/src/review/http.ts +23 -0
- package/src/review/marks.ts +43 -0
- package/src/review/media.ts +45 -0
- package/src/review/model.ts +896 -0
- package/src/review/project.ts +126 -0
- package/src/review/role.ts +48 -0
- package/src/review/routes.ts +239 -0
- package/src/review/store.ts +1011 -0
- package/src/review/threads.ts +237 -0
- package/src/review/trailer-desk.ts +385 -0
- package/src/review/trailer-model.ts +183 -0
- package/src/review/trailer-routes.ts +136 -0
- package/src/review/unit.ts +183 -0
- package/src/review/watchers.ts +54 -0
- package/src/serve/embed.ts +35 -0
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/activity.css +0 -4
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/dock-view.tsx +5 -4
- package/src/serve/hub-app/hash-route.ts +36 -1
- package/src/serve/hub-app/header-bar.tsx +3 -1
- package/src/serve/hub-app/home-st.ts +17 -3
- package/src/serve/hub-app/home-strip.tsx +3 -2
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +80 -11
- package/src/serve/hub-app/hub.css +83 -14
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/insights-view.tsx +4 -3
- package/src/serve/hub-app/keys.ts +14 -10
- package/src/serve/hub-app/layout-st.ts +63 -2
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/planned-view.tsx +3 -2
- package/src/serve/hub-app/planned.css +0 -1
- package/src/serve/hub-app/project-list.css +26 -6
- package/src/serve/hub-app/project-list.tsx +46 -10
- package/src/serve/hub-app/projects-svc.ts +7 -2
- package/src/serve/hub-app/repo-sections.ts +15 -3
- package/src/serve/hub-app/repo-st.ts +61 -4
- package/src/serve/hub-app/review/clip-st.ts +258 -0
- package/src/serve/hub-app/review/kind-view.tsx +240 -0
- package/src/serve/hub-app/review/kit.tsx +113 -0
- package/src/serve/hub-app/review/review-answer.tsx +269 -0
- package/src/serve/hub-app/review/review-board.tsx +46 -0
- package/src/serve/hub-app/review/review-list.tsx +156 -0
- package/src/serve/hub-app/review/review-page.tsx +96 -0
- package/src/serve/hub-app/review/review-st.ts +902 -0
- package/src/serve/hub-app/review/review-views.tsx +648 -0
- package/src/serve/hub-app/review/trailer-page.tsx +416 -0
- package/src/serve/hub-app/review/trailer-st.ts +602 -0
- package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
- package/src/serve/hub-app/review.css +389 -0
- package/src/serve/hub-app/theme-st.ts +56 -0
- package/src/serve/hub-app/tree-build.ts +4 -0
- package/src/serve/hub-app/tree-row.tsx +1 -1
- package/src/serve/hub-app/tree-svc.ts +4 -0
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +9 -7
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/same-origin.ts +5 -1
- package/src/serve/serve-hub.ts +15 -0
- package/src/shipkit-files.ts +9 -5
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +90 -6
- package/src/trailer/build.ts +459 -59
- package/src/trailer/ffmpeg.ts +22 -0
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +112 -43
- package/src/trailer/grammar.ts +864 -127
- package/src/trailer/graph.ts +308 -49
- package/src/trailer/holes.ts +72 -0
- package/src/trailer/load-unit.ts +20 -3
- package/src/trailer/look.ts +20 -3
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/passage.ts +129 -0
- package/src/trailer/plan.ts +82 -23
- package/src/trailer/prune.ts +204 -0
- package/src/trailer/score/compose.ts +256 -0
- package/src/trailer/score/dsp.ts +272 -0
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/palette.ts +188 -0
- package/src/trailer/score/pattern.ts +98 -0
- package/src/trailer/score/score.ts +228 -0
- package/src/trailer/score/sound.ts +123 -0
- package/src/trailer/score/synth.ts +383 -0
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/states.ts +76 -0
- package/src/trailer/stitch.ts +92 -9
- package/src/trailer/titles.ts +471 -150
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +34 -4
- package/src/trailer/words.ts +275 -0
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- package/src/wiki/graph-pages.ts +128 -0
package/src/cli/kit/verb.ts
CHANGED
|
@@ -61,6 +61,14 @@ export type Verb = {
|
|
|
61
61
|
essential?: Essential
|
|
62
62
|
/** filmable: takes `--out <path>`, a clip also `--seconds <n>` (a trailer `film` line calls it) */
|
|
63
63
|
film?: Film
|
|
64
|
+
/** with film: the row's take version, a bump films every trailer take of the row again */
|
|
65
|
+
version?: number | string
|
|
66
|
+
/** with film: `own`, a clip that films as long as it plays (no `--seconds`) */
|
|
67
|
+
length?: 'own'
|
|
68
|
+
/** with film: the words it takes and their types (trailer/words.ts), each checked at its line of a .trailer */
|
|
69
|
+
words?: Record<string, unknown>
|
|
70
|
+
/** with film: the paths its takes film, their world */
|
|
71
|
+
stamp?: string[]
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
/** the argv a row runs */
|
|
@@ -120,6 +128,10 @@ export function verbs(rows: Verb[]): CommandClass[] {
|
|
|
120
128
|
static override usage = Command.Usage({ category: v.category, description: v.description, details: v.details, examples: v.examples })
|
|
121
129
|
static essential = v.essential
|
|
122
130
|
static film = v.film
|
|
131
|
+
static filmVersion = v.version
|
|
132
|
+
static filmLength = v.length
|
|
133
|
+
static filmWords = v.words
|
|
134
|
+
static filmStamp = v.stamp
|
|
123
135
|
args = Option.Proxy()
|
|
124
136
|
async execute(): Promise<number> {
|
|
125
137
|
const { root, cwd } = this.context
|
|
@@ -148,10 +160,16 @@ export function verbs(rows: Verb[]): CommandClass[] {
|
|
|
148
160
|
|
|
149
161
|
/** the marks a command class carries: set by `verbs()` from its row, or
|
|
150
162
|
* declared as statics on a hand written class */
|
|
151
|
-
export function marksOf(c: object): { essential?: Essential; film?: Film } {
|
|
152
|
-
const o = c as { essential?: unknown; film?: unknown }
|
|
153
|
-
const out: { essential?: Essential; film?: Film } = {}
|
|
163
|
+
export function marksOf(c: object): { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } {
|
|
164
|
+
const o = c as { essential?: unknown; film?: unknown; filmVersion?: unknown; filmLength?: unknown; filmWords?: unknown; filmStamp?: unknown }
|
|
165
|
+
const out: { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } = {}
|
|
154
166
|
if (typeof o.essential === 'object' && o.essential !== null) out.essential = o.essential as Essential
|
|
155
|
-
if (o.film === 'still' || o.film === 'clip')
|
|
167
|
+
if (o.film === 'still' || o.film === 'clip') {
|
|
168
|
+
out.film = o.film
|
|
169
|
+
if (typeof o.filmVersion === 'number' || typeof o.filmVersion === 'string') out.version = o.filmVersion
|
|
170
|
+
if (o.filmLength === 'own') out.length = 'own'
|
|
171
|
+
if (typeof o.filmWords === 'object' && o.filmWords !== null) out.words = o.filmWords as Record<string, unknown>
|
|
172
|
+
if (Array.isArray(o.filmStamp)) out.stamp = o.filmStamp.map(String)
|
|
173
|
+
}
|
|
156
174
|
return out
|
|
157
175
|
}
|
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { AskNewCommand, AsksCheckCommand, AsksCommand } from './cli/cmd_Asks.ts'
|
|
|
8
8
|
import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenCommand, AssetsIdentityCommand } from './cli/cmd_Assets.ts'
|
|
9
9
|
import { BumpCommand } from './cli/cmd_Bump.ts'
|
|
10
10
|
import { CallsCommand } from './cli/cmd_Calls.ts'
|
|
11
|
+
import { TimeCommand } from './cli/cmd_Time.ts'
|
|
11
12
|
import { CheckCommand } from './cli/cmd_Check.ts'
|
|
12
13
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
13
14
|
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
@@ -39,7 +40,9 @@ import { SmokeCommand } from './cli/cmd_Smoke.ts'
|
|
|
39
40
|
import { ShotCommand } from './cli/cmd_Shot.ts'
|
|
40
41
|
import { StatusCommand } from './cli/cmd_Status.ts'
|
|
41
42
|
import { StartCommand } from './cli/cmd_Start.ts'
|
|
42
|
-
import {
|
|
43
|
+
import { TRAILER_COMMANDS } from './cli/cmd_Trailer.ts'
|
|
44
|
+
import { SOUND_COMMANDS } from './cli/cmd_Sound.ts'
|
|
45
|
+
import { REVIEW_COMMANDS } from './cli/cmd_Review.ts'
|
|
43
46
|
import { TuiCommand } from './cli/cmd_Tui.ts'
|
|
44
47
|
import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
45
48
|
|
|
@@ -70,9 +73,9 @@ export function buildCli(): Cli {
|
|
|
70
73
|
cli.register(MvCommand)
|
|
71
74
|
cli.register(CheckCommand)
|
|
72
75
|
cli.register(TuiCommand)
|
|
73
|
-
cli.register(
|
|
74
|
-
cli.register(
|
|
75
|
-
cli.register(
|
|
76
|
+
for (const c of TRAILER_COMMANDS) cli.register(c)
|
|
77
|
+
for (const c of SOUND_COMMANDS) cli.register(c)
|
|
78
|
+
for (const c of REVIEW_COMMANDS) cli.register(c)
|
|
76
79
|
cli.register(ServeCommand)
|
|
77
80
|
cli.register(PmCommand)
|
|
78
81
|
cli.register(DaemonCommand)
|
|
@@ -117,6 +120,7 @@ export function buildCli(): Cli {
|
|
|
117
120
|
cli.register(UnitsAliasCommand)
|
|
118
121
|
cli.register(CommandsCommand)
|
|
119
122
|
cli.register(CallsCommand)
|
|
123
|
+
cli.register(TimeCommand)
|
|
120
124
|
cli.register(ConfigKeysCommand)
|
|
121
125
|
|
|
122
126
|
return cli
|
|
@@ -31,7 +31,12 @@ export const NO_CHECK = 'in no check'
|
|
|
31
31
|
* section a row lives under (hub-app/tree-build.ts KIND_SECTION). There is no
|
|
32
32
|
* catch all kind: a row belongs to an owner, and what belongs to the hub
|
|
33
33
|
* itself is its status (serve/hub-problems.ts), never a row */
|
|
34
|
-
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection', 'process'] as const
|
|
34
|
+
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection', 'process', 'card', 'cut'] as const
|
|
35
|
+
|
|
36
|
+
/** the collection of the review cards that wait on him (collections/review.ts) */
|
|
37
|
+
export const REVIEW_COL = 'review'
|
|
38
|
+
/** the collection of the trailer cuts, one row a cut (collections/review.ts) */
|
|
39
|
+
export const TRAILER_COL = 'trailers'
|
|
35
40
|
|
|
36
41
|
export type ResourceType = (typeof RESOURCE_TYPES)[number]
|
|
37
42
|
|
|
@@ -27,6 +27,7 @@ import { mdBody } from './md-body.ts'
|
|
|
27
27
|
import { socialPostsCollection } from './posts.ts'
|
|
28
28
|
import { assetState, secretState, type SecretLevel, type SecretProbe } from './state-rules.ts'
|
|
29
29
|
import { reflectionsCollection } from './reflections.ts'
|
|
30
|
+
import { reviewCollection, trailersCollection } from './review.ts'
|
|
30
31
|
import { testsCollection } from './tests.ts'
|
|
31
32
|
import { checksCollection } from './checks.ts'
|
|
32
33
|
import { LAYOUT } from '../core/layout.ts'
|
|
@@ -417,5 +418,7 @@ export function buildCollections(root: string, c: RepoConfig, kit: ResolvedKit |
|
|
|
417
418
|
socialPostsCollection(root),
|
|
418
419
|
reflectionsCollection(root),
|
|
419
420
|
testsCollection(root, c),
|
|
421
|
+
// only where the repo has a review store or a trailer folder: the section of a repo with none is greyed
|
|
422
|
+
...[reviewCollection(root), trailersCollection(root)].filter((x): x is Collection => x !== null),
|
|
420
423
|
]
|
|
421
424
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// the review store and the trailer cuts as tree rows (wiki build/shipkit/review.md). Under the review section, one row a
|
|
2
|
+
// card that waits on HIM (the owner's verdict, his answer to a question): yellow, so `sk needs` lists it too. Under the trailer
|
|
3
|
+
// section, one row a cut, its open frame notes in the slot: they wait on agents, never on him, so a cut is never yellow.
|
|
4
|
+
// Each collection exists only where the repo has its folder, so a repo with none draws the section greyed
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { STATUS_LABEL, isFrame, stateOf } from '../review/model.ts'
|
|
8
|
+
import { readAllCached, storeDir } from '../review/store.ts'
|
|
9
|
+
import { statusOf, waitingOnOwner } from '../review/threads.ts'
|
|
10
|
+
import { cutsOf, trailerIds } from '../review/trailer-desk.ts'
|
|
11
|
+
import { REVIEW_COL, TRAILER_COL, type Collection, type Instance } from './collection-types.ts'
|
|
12
|
+
|
|
13
|
+
export function reviewCollection(root: string): Collection | null {
|
|
14
|
+
const dir = storeDir(root)
|
|
15
|
+
if (!existsSync(dir)) return null
|
|
16
|
+
return {
|
|
17
|
+
id: REVIEW_COL,
|
|
18
|
+
title: 'review',
|
|
19
|
+
kind: 'card',
|
|
20
|
+
media: 'image',
|
|
21
|
+
list: async () => {
|
|
22
|
+
const { items } = readAllCached(dir)
|
|
23
|
+
return waitingOnOwner(items).map(({ head: x }): Instance => ({
|
|
24
|
+
id: `card:${x.item.id}`,
|
|
25
|
+
text: `${x.item.title} (${STATUS_LABEL[statusOf(x, items)]})`,
|
|
26
|
+
label: x.item.title,
|
|
27
|
+
slot: x.item.origin.by === 'agent' ? (x.item.origin.lane ?? 'card') : 'question',
|
|
28
|
+
type: 'card',
|
|
29
|
+
path: `.shipkit/review/${x.item.id}`,
|
|
30
|
+
severity: 'yellow',
|
|
31
|
+
verbs: ['approve'],
|
|
32
|
+
}))
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function trailersCollection(root: string): Collection | null {
|
|
38
|
+
const ids = trailerIds(root)
|
|
39
|
+
if (ids.length === 0) return null
|
|
40
|
+
return {
|
|
41
|
+
id: TRAILER_COL,
|
|
42
|
+
title: 'trailer',
|
|
43
|
+
kind: 'cut',
|
|
44
|
+
media: 'image',
|
|
45
|
+
list: async () => {
|
|
46
|
+
const dir = storeDir(root)
|
|
47
|
+
const items = existsSync(dir) ? readAllCached(dir).items : []
|
|
48
|
+
const open = new Map<string, number>()
|
|
49
|
+
for (const x of items) {
|
|
50
|
+
const s = x.item.subject
|
|
51
|
+
if (isFrame(s) && stateOf(x, false).state !== 'closed') open.set(`${s.trailer}/${s.cut}`, (open.get(`${s.trailer}/${s.cut}`) ?? 0) + 1)
|
|
52
|
+
}
|
|
53
|
+
return ids.flatMap((id) =>
|
|
54
|
+
cutsOf(join(root, 'trailer', id)).map((c): Instance => {
|
|
55
|
+
const n = open.get(`${id}/${c.cut}`) ?? 0
|
|
56
|
+
const rendered = existsSync(join(root, 'trailer', id, 'out', `${c.name}.mp4`))
|
|
57
|
+
return {
|
|
58
|
+
id: `cut:${id}/${c.cut}`,
|
|
59
|
+
text: `${id} ${c.cut}${rendered ? '' : ' (not rendered)'}${n > 0 ? `, ${n} open note${n > 1 ? 's' : ''}` : ''}`,
|
|
60
|
+
label: ids.length > 1 ? `${id} · ${c.cut}` : c.cut,
|
|
61
|
+
slot: n > 0 ? `${n} open` : rendered ? '' : 'no render',
|
|
62
|
+
type: 'cut',
|
|
63
|
+
path: c.trailer === null ? `trailer/${id}/out/${c.name}.mp4` : `trailer/${id}/${c.cut}.trailer`,
|
|
64
|
+
state: rendered ? 'idle' : 'quiet',
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/core/app-icon.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { identityOfName, identityOfText, type Identity } from './identity-color.
|
|
|
10
10
|
import { configName, CONFIG_FILE } from './identity.ts'
|
|
11
11
|
import { LAYOUT } from './layout.ts'
|
|
12
12
|
|
|
13
|
-
/** the mark an app wears: the repo's own, largest first
|
|
14
|
-
|
|
13
|
+
/** the mark an app wears: the repo's own, largest first; an svg mark is drawn
|
|
14
|
+
* as an image too (the canvas decodes it) */
|
|
15
|
+
export const APP_ICON_FILES = [`${LAYOUT.identity}/icon.png`, `${LAYOUT.identity}/icon.svg`, 'public/logo.png', 'public/favicon.png']
|
|
15
16
|
|
|
16
17
|
export function appIconOf(root: string): string | null {
|
|
17
18
|
for (const f of APP_ICON_FILES) if (existsSync(join(root, f))) return join(root, f)
|
|
@@ -133,15 +134,18 @@ export async function drawAppIcon(bytes: Uint8Array, id: Identity): Promise<{ ca
|
|
|
133
134
|
return { canvas: out, plate: colour }
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
/** the icon as pngs, one per size asked, drawn once
|
|
137
|
-
|
|
137
|
+
/** the icon as pngs, one per size asked, drawn once. `plate` crops the macOS
|
|
138
|
+
* grid's margin away, the rounded plate edge to edge (the hub's rail) */
|
|
139
|
+
export async function appIconPngs(bytes: Uint8Array, id: Identity, sizes: number[] = [ICON_PX], crop: 'canvas' | 'plate' = 'canvas'): Promise<Buffer[]> {
|
|
138
140
|
const { canvas } = await drawAppIcon(bytes, id)
|
|
141
|
+
const o = crop === 'plate' ? (ICON_PX - PLATE) / 2 : 0
|
|
142
|
+
const side = crop === 'plate' ? PLATE : ICON_PX
|
|
139
143
|
return sizes.map((px) => {
|
|
140
|
-
if (px === ICON_PX) return canvas.toBuffer('image/png')
|
|
144
|
+
if (px === ICON_PX && crop === 'canvas') return canvas.toBuffer('image/png')
|
|
141
145
|
const small = createCanvas(px, px)
|
|
142
146
|
const ctx = small.getContext('2d')
|
|
143
147
|
ctx.imageSmoothingQuality = 'high'
|
|
144
|
-
ctx.drawImage(canvas, 0, 0, px, px)
|
|
148
|
+
ctx.drawImage(canvas, o, o, side, side, 0, 0, px, px)
|
|
145
149
|
return small.toBuffer('image/png')
|
|
146
150
|
})
|
|
147
151
|
}
|
package/src/core/layout.ts
CHANGED
|
@@ -36,6 +36,8 @@ export const LAYOUT = {
|
|
|
36
36
|
discord: sk('discord'),
|
|
37
37
|
// a review with no output file named by its caller (YYYYMMDD-HHMMSS-<name>.md)
|
|
38
38
|
reviews: sk('reviews'),
|
|
39
|
+
// the one feedback store: review cards and frame notes, one folder an item (review/store.ts)
|
|
40
|
+
review: sk('review'),
|
|
39
41
|
// gitignored scratch, never a plan home: logs, leases, detached runs, screenshots
|
|
40
42
|
tmp: sk('tmp'),
|
|
41
43
|
// a CLI's call log, `<cli>-calls.jsonl`: local, never tracked
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
// Transcripts live at <configRoot>/projects/<slug>/*.jsonl where <slug> is the
|
|
3
3
|
// repo's absolute path with EVERY non-alphanumeric char replaced by '-'.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// compaction summaries, subagent prompts). Every filter below exists because
|
|
8
|
-
// one of them polluted a real extraction pass.
|
|
5
|
+
// What counts as typed is the shared transcript reader's call (transcript.ts):
|
|
6
|
+
// this module finds the files, dedupes and renders.
|
|
9
7
|
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
10
8
|
import { homedir } from 'os'
|
|
11
9
|
import { basename, join, resolve } from 'path'
|
|
10
|
+
import { isTypedPrompt, readTranscriptLine } from './transcript.ts'
|
|
12
11
|
|
|
13
12
|
export type PromptEntry = {
|
|
14
13
|
uuid: string
|
|
@@ -56,126 +55,23 @@ export function discoverConfigRoots(home: string = homedir()): ConfigRoot[] {
|
|
|
56
55
|
/** Claude Code slugs EVERY non-alphanumeric char to '-', not just '/' */
|
|
57
56
|
export const projectSlug = (repoPath: string): string => resolve(repoPath).replace(/[^a-zA-Z0-9]/g, '-')
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
const NOISE_PREFIXES = [
|
|
61
|
-
'<local-command-caveat>',
|
|
62
|
-
'<local-command-stdout>',
|
|
63
|
-
'<command-name>',
|
|
64
|
-
'<command-message>',
|
|
65
|
-
'<system-reminder>',
|
|
66
|
-
'<task-notification>',
|
|
67
|
-
'Caveat: The messages below',
|
|
68
|
-
'[Request interrupted',
|
|
69
|
-
'This session is being continued from a previous conversation',
|
|
70
|
-
// model output parked in the queue (or wrapped as a meta entry): a
|
|
71
|
-
// sub-agent's hand-back, and a message another Claude session sent
|
|
72
|
-
'<agent-message',
|
|
73
|
-
'<cross-session-message',
|
|
74
|
-
'Another Claude session sent a message',
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
export function isTypedPrompt(text: string): boolean {
|
|
78
|
-
const trimmed = text.trim()
|
|
79
|
-
if (trimmed === '') return false
|
|
80
|
-
return !NOISE_PREFIXES.some((p) => trimmed.startsWith(p))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
type RawEntry = {
|
|
84
|
-
type?: unknown
|
|
85
|
-
entrypoint?: unknown
|
|
86
|
-
uuid?: unknown
|
|
87
|
-
timestamp?: unknown
|
|
88
|
-
sessionId?: unknown
|
|
89
|
-
isMeta?: unknown
|
|
90
|
-
isSidechain?: unknown
|
|
91
|
-
isCompactSummary?: unknown
|
|
92
|
-
/** queue-operation entries only */
|
|
93
|
-
operation?: unknown
|
|
94
|
-
content?: unknown
|
|
95
|
-
message?: { role?: unknown; content?: unknown }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** the typed prompt out of one transcript line, or null when the line is not one
|
|
99
|
-
* he typed. isSidechain = a subagent prompt the AGENT wrote; isMeta and
|
|
100
|
-
* isCompactSummary = harness-generated. Content is a string for a plain prompt,
|
|
101
|
-
* an array of blocks when the message carried pastes or attachments (the text
|
|
102
|
-
* blocks in there are still his). */
|
|
103
|
-
export function promptTextOf(line: string): { text: string; entry: RawEntry } | null {
|
|
104
|
-
let parsed: unknown
|
|
105
|
-
try {
|
|
106
|
-
parsed = JSON.parse(line)
|
|
107
|
-
} catch {
|
|
108
|
-
return null // truncated tail line of a live session
|
|
109
|
-
}
|
|
110
|
-
if (parsed == null || typeof parsed !== 'object') return null
|
|
111
|
-
const entry = parsed as RawEntry
|
|
112
|
-
if (entry.type !== 'user') return null
|
|
113
|
-
if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true) return null
|
|
114
|
-
if (entry.entrypoint === 'sdk-cli') return null // a script drove this session, nobody typed it
|
|
115
|
-
if (entry.message?.role !== 'user') return null
|
|
116
|
-
const content = entry.message.content
|
|
117
|
-
let text: string
|
|
118
|
-
if (typeof content === 'string') text = content
|
|
119
|
-
else if (Array.isArray(content)) {
|
|
120
|
-
const parts = content
|
|
121
|
-
.filter((b): b is { type: string; text: string } => b?.type === 'text' && typeof b?.text === 'string')
|
|
122
|
-
.map((b) => b.text)
|
|
123
|
-
if (parts.length === 0) return null // pure tool_result turn
|
|
124
|
-
text = parts.join('\n')
|
|
125
|
-
} else return null
|
|
126
|
-
// the harness appends reminder blocks INSIDE real prompts — strip, keep the text
|
|
127
|
-
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim()
|
|
128
|
-
// a slash-command entry is a harness WRAPPER around typed text:
|
|
129
|
-
// <command-name>/cmd</command-name><command-args>THE TEXT</command-args>.
|
|
130
|
-
// Dropping the whole entry throws away real instructions and repro pastes.
|
|
131
|
-
const args = text.match(/<command-args>([\s\S]*?)<\/command-args>/)
|
|
132
|
-
if (args?.[1] != null) {
|
|
133
|
-
const body = args[1].trim()
|
|
134
|
-
if (body === '') return null // bare invocation, nothing typed
|
|
135
|
-
const name = text.match(/<command-name>([\s\S]*?)<\/command-name>/)
|
|
136
|
-
text = name?.[1] == null ? body : `${name[1].trim()} ${body}`
|
|
137
|
-
}
|
|
138
|
-
if (!isTypedPrompt(text)) return null
|
|
139
|
-
return { text, entry }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** A prompt he typed while the agent was still working: the harness parks it as
|
|
143
|
-
* `{type:'queue-operation', operation:'enqueue', content}` and, if the run ends
|
|
144
|
-
* first, it may NEVER become a user message. Skipping these loses exactly the
|
|
145
|
-
* mid-flight corrections and standing conventions a retrospective is for */
|
|
146
|
-
export function queuedTextOf(line: string): { text: string; entry: RawEntry } | null {
|
|
147
|
-
let parsed: unknown
|
|
148
|
-
try {
|
|
149
|
-
parsed = JSON.parse(line)
|
|
150
|
-
} catch {
|
|
151
|
-
return null
|
|
152
|
-
}
|
|
153
|
-
if (parsed == null || typeof parsed !== 'object') return null
|
|
154
|
-
const entry = parsed as RawEntry
|
|
155
|
-
if (entry.type !== 'queue-operation' || entry.operation !== 'enqueue') return null
|
|
156
|
-
if (typeof entry.content !== 'string') return null
|
|
157
|
-
const text = entry.content.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim()
|
|
158
|
-
if (!isTypedPrompt(text)) return null
|
|
159
|
-
return { text, entry }
|
|
160
|
-
}
|
|
58
|
+
export { isTypedPrompt } from './transcript.ts'
|
|
161
59
|
|
|
162
60
|
export function extractPrompts(jsonl: string, ctx: { sessionId: string; account: string }): PromptEntry[] {
|
|
163
61
|
const out: PromptEntry[] = []
|
|
164
62
|
for (const line of jsonl.split('\n')) {
|
|
165
63
|
if (line.trim() === '') continue
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
origin: queued == null ? 'typed' : 'queued',
|
|
178
|
-
})
|
|
64
|
+
for (const ev of readTranscriptLine(line)) {
|
|
65
|
+
if (ev.kind !== 'prompt') continue
|
|
66
|
+
out.push({
|
|
67
|
+
uuid: ev.uuid ?? `${ev.origin === 'typed' ? 'typed' : 'queue'}:${ctx.sessionId}:${ev.timestamp}`,
|
|
68
|
+
timestamp: ev.timestamp,
|
|
69
|
+
sessionId: ev.sessionId ?? ctx.sessionId,
|
|
70
|
+
account: ctx.account,
|
|
71
|
+
text: ev.text,
|
|
72
|
+
origin: ev.origin,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
179
75
|
}
|
|
180
76
|
return out
|
|
181
77
|
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// THE transcript reader: one Claude Code jsonl line in, the events it holds out.
|
|
2
|
+
// `sk reflect` keeps the prompts, `sk time` keeps everything. One parser for
|
|
3
|
+
// both, so a harness shape fixed here is fixed for every reader.
|
|
4
|
+
//
|
|
5
|
+
// A line is one entry. An assistant entry holds ONE content block (thinking,
|
|
6
|
+
// text or tool_use), the user entry after a tool_use holds its tool_result,
|
|
7
|
+
// and the harness writes many user-role entries the human never typed (tool
|
|
8
|
+
// results, command wrappers, compaction summaries, sub-agent hand-backs).
|
|
9
|
+
// Every prompt filter below exists because one of them polluted a real pass.
|
|
10
|
+
|
|
11
|
+
export type PromptOrigin = 'typed' | 'queued'
|
|
12
|
+
|
|
13
|
+
/** how a call was allowed: by the human (`user`), by the auto mode
|
|
14
|
+
* classifier (a model call before the tool runs), by a rule or a hook, by the
|
|
15
|
+
* permission mode, or not recorded */
|
|
16
|
+
export type Perm = 'user' | 'classifier' | 'rule' | 'mode' | ''
|
|
17
|
+
|
|
18
|
+
export function permOf(d: { source?: unknown; reasonType?: unknown } | undefined): Perm {
|
|
19
|
+
if (d == null) return ''
|
|
20
|
+
if (typeof d.source === 'string' && d.source.startsWith('user')) return 'user'
|
|
21
|
+
if (d.reasonType === 'classifier') return 'classifier'
|
|
22
|
+
if (d.reasonType === 'mode') return 'mode'
|
|
23
|
+
return typeof d.source === 'string' ? 'rule' : ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** the harness's wrapper around a message relayed into a running agent */
|
|
27
|
+
const RELAYED = /^[^\n]{1,80}? sent (?:a new |a )?message while you were working\b/
|
|
28
|
+
|
|
29
|
+
/** how a tool call ended, read from its tool_result */
|
|
30
|
+
export type Outcome = 'ok' | 'exit' | 'timeout' | 'blocked' | 'denied' | 'interrupted' | 'error'
|
|
31
|
+
|
|
32
|
+
export type TranscriptEvent =
|
|
33
|
+
/** a prompt the human typed; `queued` = typed while the agent worked */
|
|
34
|
+
| { kind: 'prompt'; at: number; timestamp: string; uuid: string | null; sessionId: string | null; text: string; origin: PromptOrigin }
|
|
35
|
+
/** a user-role turn nobody typed that wakes the agent: a task notification,
|
|
36
|
+
* a sub-agent hand-back, a peer message, a script-driven prompt */
|
|
37
|
+
| { kind: 'wake'; at: number }
|
|
38
|
+
/** a message relayed into the agent (the coordinator, the human, another
|
|
39
|
+
* session "sent a message while you were working"): it can end a park of
|
|
40
|
+
* hours after the agent handed back */
|
|
41
|
+
| { kind: 'message'; at: number }
|
|
42
|
+
| { kind: 'use'; at: number; id: string; name: string; detail: string }
|
|
43
|
+
/** `perm`: how the call was allowed; `user` = it waited for the human, so its time is his */
|
|
44
|
+
| { kind: 'result'; at: number; id: string; outcome: Outcome; code: number | null; perm: Perm }
|
|
45
|
+
/** an assistant text block: the agent talking, which can end its turn */
|
|
46
|
+
| { kind: 'reply'; at: number }
|
|
47
|
+
|
|
48
|
+
// wrappers the harness injects as user-role entries: none of them are typed
|
|
49
|
+
const NOISE_PREFIXES = [
|
|
50
|
+
'<local-command-caveat>',
|
|
51
|
+
'<local-command-stdout>',
|
|
52
|
+
'<command-name>',
|
|
53
|
+
'<command-message>',
|
|
54
|
+
'<system-reminder>',
|
|
55
|
+
'<task-notification>',
|
|
56
|
+
'Caveat: The messages below',
|
|
57
|
+
'[Request interrupted',
|
|
58
|
+
'This session is being continued from a previous conversation',
|
|
59
|
+
// model output parked in the queue (or wrapped as a meta entry): a
|
|
60
|
+
// sub-agent's hand-back, and a message another Claude session sent
|
|
61
|
+
'<agent-message',
|
|
62
|
+
'<cross-session-message',
|
|
63
|
+
'Another Claude session sent a message',
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
export function isTypedPrompt(text: string): boolean {
|
|
67
|
+
const trimmed = text.trim()
|
|
68
|
+
if (trimmed === '') return false
|
|
69
|
+
return !NOISE_PREFIXES.some((p) => trimmed.startsWith(p))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type Block = { type?: unknown; text?: unknown; id?: unknown; name?: unknown; input?: unknown; tool_use_id?: unknown; is_error?: unknown; content?: unknown }
|
|
73
|
+
|
|
74
|
+
export type RawEntry = {
|
|
75
|
+
type?: unknown
|
|
76
|
+
entrypoint?: unknown
|
|
77
|
+
uuid?: unknown
|
|
78
|
+
timestamp?: unknown
|
|
79
|
+
sessionId?: unknown
|
|
80
|
+
isMeta?: unknown
|
|
81
|
+
isSidechain?: unknown
|
|
82
|
+
isCompactSummary?: unknown
|
|
83
|
+
/** `peer` on a sub-agent hand-back or another session's message */
|
|
84
|
+
turnOrigin?: unknown
|
|
85
|
+
/** queue-operation entries only */
|
|
86
|
+
operation?: unknown
|
|
87
|
+
content?: unknown
|
|
88
|
+
message?: { role?: unknown; content?: unknown }
|
|
89
|
+
toolUseResult?: unknown
|
|
90
|
+
/** on a tool_result entry: who allowed the call (`config`, `user_temporary`, …) */
|
|
91
|
+
permissionDecision?: { source?: unknown; reasonType?: unknown }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const stripReminders = (text: string): string => text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim()
|
|
95
|
+
|
|
96
|
+
/** the user text of a user entry, reminders stripped, a slash command's typed
|
|
97
|
+
* args rescued; null when it carries no text at all (a pure tool_result turn).
|
|
98
|
+
* Content is a string for a plain prompt, an array of blocks when the message
|
|
99
|
+
* carried pastes or attachments (the text blocks in there are still his). */
|
|
100
|
+
function userText(entry: RawEntry): string | null {
|
|
101
|
+
const content = entry.message?.content
|
|
102
|
+
let text: string
|
|
103
|
+
if (typeof content === 'string') text = content
|
|
104
|
+
else if (Array.isArray(content)) {
|
|
105
|
+
const parts = (content as Block[]).filter((b) => b?.type === 'text' && typeof b?.text === 'string').map((b) => b.text as string)
|
|
106
|
+
if (parts.length === 0) return null
|
|
107
|
+
text = parts.join('\n')
|
|
108
|
+
} else return null
|
|
109
|
+
text = stripReminders(text)
|
|
110
|
+
// a slash-command entry is a harness WRAPPER around typed text:
|
|
111
|
+
// <command-name>/cmd</command-name><command-args>THE TEXT</command-args>.
|
|
112
|
+
// Dropping the whole entry throws away real instructions and repro pastes.
|
|
113
|
+
const args = text.match(/<command-args>([\s\S]*?)<\/command-args>/)
|
|
114
|
+
if (args?.[1] != null) {
|
|
115
|
+
const body = args[1].trim()
|
|
116
|
+
if (body === '') return ''
|
|
117
|
+
const name = text.match(/<command-name>([\s\S]*?)<\/command-name>/)
|
|
118
|
+
text = name?.[1] == null ? body : `${name[1].trim()} ${body}`
|
|
119
|
+
}
|
|
120
|
+
return text
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** the typed prompt out of a parsed user entry, or null when he did not type
|
|
124
|
+
* it. isSidechain = a subagent prompt the AGENT wrote; isMeta and
|
|
125
|
+
* isCompactSummary = harness-generated; entrypoint sdk-cli = a script drove it */
|
|
126
|
+
export function typedPromptOf(entry: RawEntry): string | null {
|
|
127
|
+
if (entry.type !== 'user') return null
|
|
128
|
+
if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true) return null
|
|
129
|
+
if (entry.entrypoint === 'sdk-cli') return null
|
|
130
|
+
if (entry.message?.role !== 'user') return null
|
|
131
|
+
const text = userText(entry)
|
|
132
|
+
if (text == null || !isTypedPrompt(text)) return null
|
|
133
|
+
return text
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A prompt he typed while the agent was still working: the harness parks it as
|
|
137
|
+
* `{type:'queue-operation', operation:'enqueue', content}` and, if the run ends
|
|
138
|
+
* first, it may NEVER become a user message. Skipping these loses exactly the
|
|
139
|
+
* mid-flight corrections and standing conventions a retrospective is for */
|
|
140
|
+
export function queuedPromptOf(entry: RawEntry): string | null {
|
|
141
|
+
if (entry.type !== 'queue-operation' || entry.operation !== 'enqueue') return null
|
|
142
|
+
if (typeof entry.content !== 'string') return null
|
|
143
|
+
const text = stripReminders(entry.content)
|
|
144
|
+
return isTypedPrompt(text) ? text : null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const BASH_EXIT = /^Exit code (\d+)/
|
|
148
|
+
const BLOCKED = [/^PreToolUse:/, /^Permission to use /, /^Permission for this action was denied/, /auto mode classifier/i, /hook error/i]
|
|
149
|
+
|
|
150
|
+
/** how a tool call ended. The harness marks every failure `is_error`, and the
|
|
151
|
+
* text says which kind: a non zero exit, a hook or permission block, the
|
|
152
|
+
* human saying no, a timeout (now moved to the background), anything else */
|
|
153
|
+
export function outcomeOf(r: { isError: boolean; text: string; extra: unknown }): { outcome: Outcome; code: number | null } {
|
|
154
|
+
const extra = r.extra != null && typeof r.extra === 'object' ? (r.extra as Record<string, unknown>) : {}
|
|
155
|
+
if (typeof extra.timedOutAfterMs === 'number' || /^Command timed out after/.test(r.text)) return { outcome: 'timeout', code: null }
|
|
156
|
+
if (extra.interrupted === true || r.text.startsWith('[Request interrupted')) return { outcome: 'interrupted', code: null }
|
|
157
|
+
if (!r.isError) return { outcome: 'ok', code: null }
|
|
158
|
+
const exit = BASH_EXIT.exec(r.text)
|
|
159
|
+
if (exit?.[1] != null) return { outcome: 'exit', code: Number(exit[1]) }
|
|
160
|
+
if (/^The user doesn't want to proceed/.test(r.text) || /^The user (rejected|denied)/.test(r.text)) return { outcome: 'denied', code: null }
|
|
161
|
+
if (BLOCKED.some((re) => re.test(r.text))) return { outcome: 'blocked', code: null }
|
|
162
|
+
return { outcome: 'error', code: null }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const DETAIL_MAX = 400
|
|
166
|
+
const clip = (s: string): string => (s.length > DETAIL_MAX ? s.slice(0, DETAIL_MAX) : s)
|
|
167
|
+
|
|
168
|
+
/** a long command keeps its head and its tail: what runs after a heredoc
|
|
169
|
+
* (`cat > x.ts <<EOF … EOF` then `bun x.ts`) is at the end */
|
|
170
|
+
const clipMiddle = (s: string): string => (s.length > 2 * DETAIL_MAX ? `${s.slice(0, DETAIL_MAX)}\n${s.slice(-DETAIL_MAX)}` : s)
|
|
171
|
+
|
|
172
|
+
/** the one input field that says what a call did: the Bash command, the
|
|
173
|
+
* sub-agent type and its description, the skill, the url */
|
|
174
|
+
export function detailOf(name: string, input: unknown): string {
|
|
175
|
+
const i = input != null && typeof input === 'object' ? (input as Record<string, unknown>) : {}
|
|
176
|
+
const str = (k: string): string => (typeof i[k] === 'string' ? (i[k] as string) : '')
|
|
177
|
+
if (name === 'Bash') return clipMiddle(str('command'))
|
|
178
|
+
if (name === 'Agent' || name === 'Task') return clip(`${str('subagent_type') || 'general-purpose'}: ${str('description')}`)
|
|
179
|
+
if (name === 'Skill') return clip(str('skill'))
|
|
180
|
+
if (name === 'WebFetch') return clip(str('url'))
|
|
181
|
+
if (name === 'Monitor' || name === 'TaskOutput' || name === 'TaskStop') return clip(str('description') || str('task_id'))
|
|
182
|
+
return ''
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const resultText = (content: unknown): string => {
|
|
186
|
+
if (typeof content === 'string') return content
|
|
187
|
+
if (Array.isArray(content)) {
|
|
188
|
+
const first = (content as Block[]).find((b) => b?.type === 'text' && typeof b.text === 'string')
|
|
189
|
+
return first == null ? '' : (first.text as string)
|
|
190
|
+
}
|
|
191
|
+
return ''
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// the entry types that carry nothing a reader wants and make most of the
|
|
195
|
+
// bytes (api-request alone is about 80 percent of a long session): a line
|
|
196
|
+
// that starts with one of these is skipped before JSON.parse
|
|
197
|
+
const KEPT_TYPE_PREFIXES = ['{"type":"user"', '{"type":"assistant"', '{"type":"queue-operation"']
|
|
198
|
+
|
|
199
|
+
/** every event one transcript line holds; [] for anything else, a truncated
|
|
200
|
+
* tail line of a live session included */
|
|
201
|
+
export function readTranscriptLine(line: string): TranscriptEvent[] {
|
|
202
|
+
if (line.startsWith('{"type":"') && !KEPT_TYPE_PREFIXES.some((p) => line.startsWith(p))) return []
|
|
203
|
+
let parsed: unknown
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(line)
|
|
206
|
+
} catch {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
if (parsed == null || typeof parsed !== 'object') return []
|
|
210
|
+
return eventsOf(parsed as RawEntry)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function eventsOf(entry: RawEntry): TranscriptEvent[] {
|
|
214
|
+
const timestamp = typeof entry.timestamp === 'string' ? entry.timestamp : ''
|
|
215
|
+
const at = Date.parse(timestamp)
|
|
216
|
+
const uuid = typeof entry.uuid === 'string' ? entry.uuid : null
|
|
217
|
+
const sessionId = typeof entry.sessionId === 'string' ? entry.sessionId : null
|
|
218
|
+
const prompt = (text: string, origin: PromptOrigin): TranscriptEvent => ({ kind: 'prompt', at, timestamp, uuid, sessionId, text, origin })
|
|
219
|
+
|
|
220
|
+
if (entry.type === 'queue-operation') {
|
|
221
|
+
const q = queuedPromptOf(entry)
|
|
222
|
+
return q == null ? [] : [prompt(q, 'queued')]
|
|
223
|
+
}
|
|
224
|
+
if (Number.isNaN(at) && entry.type !== 'user') return []
|
|
225
|
+
if (entry.type === 'assistant') {
|
|
226
|
+
const blocks = Array.isArray(entry.message?.content) ? (entry.message.content as Block[]) : []
|
|
227
|
+
const out: TranscriptEvent[] = []
|
|
228
|
+
for (const b of blocks) {
|
|
229
|
+
if (b?.type === 'tool_use' && typeof b.id === 'string' && typeof b.name === 'string') out.push({ kind: 'use', at, id: b.id, name: b.name, detail: detailOf(b.name, b.input) })
|
|
230
|
+
else if (b?.type === 'text') out.push({ kind: 'reply', at })
|
|
231
|
+
}
|
|
232
|
+
return out
|
|
233
|
+
}
|
|
234
|
+
if (entry.type !== 'user') return []
|
|
235
|
+
const out: TranscriptEvent[] = []
|
|
236
|
+
const content = entry.message?.content
|
|
237
|
+
if (Array.isArray(content)) {
|
|
238
|
+
const perm = permOf(entry.permissionDecision)
|
|
239
|
+
for (const b of content as Block[]) {
|
|
240
|
+
if (b?.type !== 'tool_result' || typeof b.tool_use_id !== 'string' || Number.isNaN(at)) continue
|
|
241
|
+
const o = outcomeOf({ isError: b.is_error === true, text: resultText(b.content), extra: entry.toolUseResult })
|
|
242
|
+
out.push({ kind: 'result', at, id: b.tool_use_id, ...o, perm })
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const typed = typedPromptOf(entry)
|
|
246
|
+
if (typed != null) out.push(prompt(typed, 'typed'))
|
|
247
|
+
else if (out.length === 0 && !Number.isNaN(at) && entry.isMeta === true && RELAYED.test(userText(entry) ?? '')) out.push({ kind: 'message', at })
|
|
248
|
+
// a peer message (a sub-agent hand-back, another session's message) is written isMeta, yet it wakes the agent
|
|
249
|
+
else if (out.length === 0 && !Number.isNaN(at) && (entry.isMeta !== true || entry.turnOrigin === 'peer') && entry.isCompactSummary !== true) {
|
|
250
|
+
const text = userText(entry)
|
|
251
|
+
if (text != null && text !== '') out.push({ kind: 'wake', at })
|
|
252
|
+
}
|
|
253
|
+
return out
|
|
254
|
+
}
|