panoma 0.9.0 → 0.10.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/README.md +1 -0
- package/app/BUILD-INFO.json +3 -3
- package/app/apps/web/.next-bundle/BUILD_ID +1 -1
- package/app/apps/web/.next-bundle/app-build-manifest.json +414 -322
- package/app/apps/web/.next-bundle/app-path-routes-manifest.json +45 -33
- package/app/apps/web/.next-bundle/build-manifest.json +3 -3
- package/app/apps/web/.next-bundle/prerender-manifest.json +9 -9
- package/app/apps/web/.next-bundle/required-server-files.json +5 -4
- package/app/apps/web/.next-bundle/routes-manifest.json +14 -0
- package/app/apps/web/.next-bundle/server/app/(app)/agents/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/agents/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/ai/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/ai/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/apps/[id]/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/apps/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/apps/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/apps/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/bridge/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/bridge/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/copies/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/copies/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/credentials/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/credentials/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/disk/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/disk/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/handoff/page.js +2 -0
- package/app/apps/web/.next-bundle/server/app/(app)/handoff/page_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/(app)/hidden/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/hidden/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/p/[slug]/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/p/[slug]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/p/[slug]/video/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/p/[slug]/video/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/packages/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/packages/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/runs/[id]/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/runs/[id]/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/runs/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/runs/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/search/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/search/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/spend/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/spend/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/twin/look/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/twin/look/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/twin/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/twin/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/(app)/unsaved/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/(app)/unsaved/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/_not-found/page.js +2 -2
- package/app/apps/web/.next-bundle/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/accounts/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/accounts/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/apps/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/apps/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/consult/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/consult/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/context/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/context/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/conversations/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/conversations/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/handoff/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/handoff/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/hello/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/hello/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/journal/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/journal/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/keys/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/keys/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/log/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/log/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/mcp/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/agent/mcp/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/notes/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/notes/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/tasks/[id]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/tasks/[id]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/tasks/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/tasks/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/agent/video/cancel/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/video/cancel/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/video/jobs/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/video/jobs/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/video/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/agent/video/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/ai/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/ai/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/[operation]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/[operation]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/credentials/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/credentials/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/jobs/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/jobs/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/legal/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/legal/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/settings/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/[id]/settings/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/artifact/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/artifact/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/cancel/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/cancel/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/jobs/[jobId]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/apps/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/assets/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/assets/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/assignments/launch/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/assignments/launch/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/assignments/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/assignments/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/catalog/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/catalog/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/check/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/check/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/consultations/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/consultations/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/describe/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/describe/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/disk/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/disk/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/enrich/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/enrich/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/environment/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/environment/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/handoff/[id]/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/[id]/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/digest/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/digest/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/launch/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/launch/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/record/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/record/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/route.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/handoff/route_client-reference-manifest.js +1 -0
- package/app/apps/web/.next-bundle/server/app/api/hooks/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/ingest/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/ingest/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/apply/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/apply/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/context/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/context/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/inspect/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/inspect/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/repair/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/repair/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/md/review/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/md/review/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/memory/export/route.js +2 -2
- package/app/apps/web/.next-bundle/server/app/api/memory/export/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/north/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/north/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/notes/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/notes/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/open/all/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/open/all/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/open/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/open/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/project/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/project/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/rescan/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/rescan/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/roots/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/roots/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/runs/[id]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/runs/[id]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/runs/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/runs/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/scale/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/scale/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/search/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/secrets/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/secrets/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/spend/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/spend/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/tasks/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/tasks/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/today/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/today/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/assign/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/assign/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/classify/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/classify/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/critique/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/critique/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/design/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/design/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/distill/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/distill/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/episodes/learn/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/episodes/learn/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/episodes/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/episodes/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/look/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/look/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/mine/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/mine/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/rehearse/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/rehearse/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/score/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/score/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/shot/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/shot/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/sources/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/sources/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/synthesize/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/synthesize/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/taste/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/taste/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/verdicts/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/twin/verdicts/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/watch/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/api/watch/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/apple-icon.png/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/icon/[id]/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app/icon/[id]/route_client-reference-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/app/icon.png/route.js +1 -1
- package/app/apps/web/.next-bundle/server/app-paths-manifest.json +45 -33
- package/app/apps/web/.next-bundle/server/chunks/1190.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/1214.js +217 -0
- package/app/apps/web/.next-bundle/server/chunks/1646.js +5 -5
- package/app/apps/web/.next-bundle/server/chunks/2455.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/2658.js +2 -2
- package/app/apps/web/.next-bundle/server/chunks/305.js +3 -0
- package/app/apps/web/.next-bundle/server/chunks/329.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/3347.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/376.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/3815.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/3834.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/3863.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/3896.js +16 -0
- package/app/apps/web/.next-bundle/server/chunks/4478.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/4498.js +2 -2
- package/app/apps/web/.next-bundle/server/chunks/4810.js +5 -5
- package/app/apps/web/.next-bundle/server/chunks/5103.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/{5975.js → 5676.js} +1 -1
- package/app/apps/web/.next-bundle/server/chunks/5839.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/6206.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/6384.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/6913.js +7 -7
- package/app/apps/web/.next-bundle/server/chunks/{2256.js → 7011.js} +2 -2
- package/app/apps/web/.next-bundle/server/chunks/7245.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/7400.js +3 -0
- package/app/apps/web/.next-bundle/server/chunks/7498.js +2 -0
- package/app/apps/web/.next-bundle/server/chunks/7957.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/8404.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/8441.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/8463.js +1 -1
- package/app/apps/web/.next-bundle/server/chunks/9006.js +3 -0
- package/app/apps/web/.next-bundle/server/chunks/9128.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/{6693.js → 9226.js} +2 -2
- package/app/apps/web/.next-bundle/server/chunks/9441.js +1 -0
- package/app/apps/web/.next-bundle/server/chunks/9511.js +16 -0
- package/app/apps/web/.next-bundle/server/functions-config-manifest.json +14 -8
- package/app/apps/web/.next-bundle/server/middleware-build-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/middleware-manifest.json +5 -5
- package/app/apps/web/.next-bundle/server/middleware.js +2 -2
- package/app/apps/web/.next-bundle/server/next-font-manifest.js +1 -1
- package/app/apps/web/.next-bundle/server/next-font-manifest.json +1 -1
- package/app/apps/web/.next-bundle/server/pages/500.html +1 -1
- package/app/apps/web/.next-bundle/server/server-reference-manifest.json +1 -1
- package/app/apps/web/.next-bundle/static/chunks/1369-514f421b74ddd546.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/{2202-897aaad3cd785ea6.js → 2202-320ee3883061ca36.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/2575-e53eff318f43e123.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/2921-92c9a83559025278.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/2948-db3dde2d3c76f184.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/4189-0adcbb5b786fe4ed.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/7f092448-875cfd730dc78b44.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/agents/{page-28b11f831b7bc8c4.js → page-be3b13d1301a5855.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/ai/{page-8b5fe63986bf3757.js → page-beb96e7f7a56ac65.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/apps/[id]/{page-dc2c3a7874636c29.js → page-7fd561dfea561bee.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/apps/page-ba0da767076b59ee.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/bridge/page-b8cc7ef93c7a01f7.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/copies/page-1c5bac527b81fbfb.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/credentials/{page-0c3307d9cff31e71.js → page-db952949fd680244.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/disk/{page-13cd17998c4b3c05.js → page-58dba9a693f3efd1.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/error-2c3f75f2c68c6bab.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/handoff/page-af0790c554af2371.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/hidden/page-a2f95d21457cf062.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/layout-2f357c79701ed83e.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/p/[slug]/page-ea0ecbae2d960666.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/p/[slug]/video/page-51409af610218587.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/packages/page-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/page-ad2cd4e236fd2f60.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/runs/[id]/{page-0f664082168a49b2.js → page-75c179ccca63a724.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/runs/page-1c5bac527b81fbfb.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/search/{page-0c57c71f581cdf40.js → page-5f61bffc868d47cc.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/spend/page-fce603598ab4b457.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/twin/look/{page-a1f666c611d55a0c.js → page-e8582a3dd4da47bb.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/twin/{page-c7212909b051736c.js → page-6e103fc5ed54eabe.js} +1 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/unsaved/page-433663c1b9556d04.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/_not-found/page-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/accounts/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/apps/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/consult/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/context/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/conversations/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/handoff/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/hello/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/journal/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/keys/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/log/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/mcp/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/notes/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/tasks/[id]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/tasks/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/video/cancel/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/video/jobs/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/video/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/ai/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/[operation]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/credentials/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/jobs/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/legal/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/settings/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/artifact/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/cancel/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/assets/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/assignments/launch/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/assignments/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/catalog/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/check/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/consultations/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/describe/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/disk/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/enrich/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/environment/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/handoff/[id]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/handoff/digest/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/handoff/launch/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/handoff/record/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/handoff/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/hooks/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/ingest/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/apply/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/context/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/inspect/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/repair/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/review/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/memory/export/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/north/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/notes/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/open/all/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/open/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/project/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/rescan/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/roots/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/runs/[id]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/runs/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/scale/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/search/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/secrets/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/spend/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/tasks/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/today/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/assign/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/classify/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/critique/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/design/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/distill/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/episodes/learn/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/episodes/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/look/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/mine/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/rehearse/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/score/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/shot/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/sources/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/synthesize/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/taste/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/verdicts/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/api/watch/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/global-not-found-1c5bac527b81fbfb.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/icon/[id]/route-eac7618918d667c0.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/app/not-found-1c5bac527b81fbfb.js +1 -0
- package/app/apps/web/.next-bundle/static/chunks/{main-app-a35ca295bbc2c578.js → main-app-0bad81d9bc79bc82.js} +1 -1
- package/app/apps/web/.next-bundle/static/css/5fdcdf46b29ffe26.css +1 -0
- package/app/apps/web/.next-bundle/static/phRAJdip0Dtgh1L1Yw0jm/_buildManifest.js +1 -0
- package/app/apps/web/server.js +1 -1
- package/app/node_modules/@panoma/ai/dist/index.d.ts +6 -1
- package/app/node_modules/@panoma/ai/dist/index.js +43 -6
- package/app/node_modules/@panoma/core/dist/index.d.ts +39 -1
- package/app/node_modules/@panoma/core/dist/index.js +149 -57
- package/app/node_modules/@panoma/core/dist/untrusted.d.ts +14 -1
- package/app/node_modules/@panoma/db/dist/{chunk-V6TJYC6N.js → chunk-VTHU57AZ.js} +59 -3
- package/app/node_modules/@panoma/db/dist/{client-B1_3MF0o.d.ts → client-B2H4A_7h.d.ts} +379 -3
- package/app/node_modules/@panoma/db/dist/client.d.ts +1 -1
- package/app/node_modules/@panoma/db/dist/client.js +1 -1
- package/app/node_modules/@panoma/db/dist/index.d.ts +56 -4
- package/app/node_modules/@panoma/db/dist/index.js +104 -27
- package/app/node_modules/@panoma/db/migrations/0060_handoffs.sql +23 -0
- package/app/node_modules/@panoma/db/migrations/0061_handoff_surface.sql +1 -0
- package/app/node_modules/@panoma/db/migrations/0062_handoff_requested_by.sql +1 -0
- package/app/node_modules/@panoma/db/migrations/0063_app_job_requested_by.sql +1 -0
- package/app/node_modules/@panoma/db/migrations/meta/0060_snapshot.json +4258 -0
- package/app/node_modules/@panoma/db/migrations/meta/0061_snapshot.json +4265 -0
- package/app/node_modules/@panoma/db/migrations/meta/0062_snapshot.json +4271 -0
- package/app/node_modules/@panoma/db/migrations/meta/0063_snapshot.json +4277 -0
- package/app/node_modules/@panoma/db/migrations/meta/_journal.json +28 -0
- package/app/node_modules/@panoma/handoff/dist/chunk-4PTSNPI3.js +73 -0
- package/app/node_modules/@panoma/handoff/dist/faults.d.ts +26 -0
- package/app/node_modules/@panoma/handoff/dist/faults.js +14 -0
- package/app/node_modules/@panoma/handoff/dist/index.d.ts +579 -0
- package/app/node_modules/@panoma/handoff/dist/index.js +3364 -0
- package/app/node_modules/@panoma/handoff/package.json +23 -0
- package/app/node_modules/@panoma/mcp/dist/index.js +709 -13
- package/app/node_modules/drizzle-orm/node_modules/.bin/gel +3 -3
- package/dist/{ai-command-IRSVAWYU.js → ai-command-TSSMG7H3.js} +4 -4
- package/dist/{apps-FI2Q2CFP.js → apps-7E4RUVJY.js} +4 -4
- package/dist/{check-command-PPILA25M.js → check-command-RIUGB4HV.js} +4 -4
- package/dist/{chunk-P7ZGHSMO.js → chunk-3QYJKRW6.js} +209 -1
- package/dist/{chunk-5EG7NRL4.js → chunk-3T3WTSPS.js} +147 -57
- package/dist/{chunk-BCHIG2AU.js → chunk-47UUF7B7.js} +2 -2
- package/dist/{chunk-JY7ALFNX.js → chunk-6RVG2VUY.js} +44 -7
- package/dist/{chunk-2MKBCYVH.js → chunk-FWSQH4NS.js} +3 -3
- package/dist/{chunk-YQOQAFTY.js → chunk-HHHT2MTQ.js} +2 -2
- package/dist/{chunk-AW6KPPIR.js → chunk-WAI2SCFY.js} +22 -4
- package/dist/{dist-NQIOBJFO.js → dist-F3N6LCV6.js} +2 -2
- package/dist/{dist-G32QKT7H.js → dist-PQLBVRZQ.js} +7 -1
- package/dist/{dist-F5A5RVCK.js → dist-RNXJD4VL.js} +1 -1
- package/dist/handoff-command-NGSYPGWW.js +3876 -0
- package/dist/index.js +47 -16
- package/dist/{md-command-Q4M2YBEH.js → md-command-PYD3SPHA.js} +3 -3
- package/dist/{memory-command-IXVQSADX.js → memory-command-BWP52OQS.js} +3 -3
- package/dist/{next-command-2PXBP5OR.js → next-command-NFDGQMHO.js} +4 -4
- package/dist/{north-command-XFUNSNPH.js → north-command-SKBVZEGK.js} +4 -4
- package/dist/{review-command-6AH7MVKP.js → review-command-SMYI7XKH.js} +2 -2
- package/dist/{spend-command-DA36GLNI.js → spend-command-NYAELLRP.js} +3 -3
- package/dist/{twin-command-HYEG2WPA.js → twin-command-M25PTHLP.js} +9 -8
- package/dist/{video-TWUW2L36.js → video-QKEQ2FGR.js} +6 -6
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -2
- package/app/apps/web/.next-bundle/server/chunks/1602.js +0 -3
- package/app/apps/web/.next-bundle/server/chunks/3088.js +0 -16
- package/app/apps/web/.next-bundle/server/chunks/3768.js +0 -3
- package/app/apps/web/.next-bundle/server/chunks/5251.js +0 -217
- package/app/apps/web/.next-bundle/server/chunks/5741.js +0 -1
- package/app/apps/web/.next-bundle/server/chunks/5941.js +0 -1
- package/app/apps/web/.next-bundle/server/chunks/7581.js +0 -1
- package/app/apps/web/.next-bundle/server/chunks/8384.js +0 -1
- package/app/apps/web/.next-bundle/server/chunks/8990.js +0 -2
- package/app/apps/web/.next-bundle/static/chunks/1369-8f6dde7e48bc4684.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/2340-11b365d374b2be7b.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/2575-62182d00e5cf3115.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/261-1c2829beaca77dc9.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/7f092448-e07c21e2897ac52e.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/apps/page-00ec4bf42a14ac17.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/bridge/page-86a819d6d7f65cd0.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/copies/page-69ee3fc638b1eecf.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/error-4706c73d842251e2.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/hidden/page-55639b16ccebad95.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/layout-7f3d387ae540018d.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/p/[slug]/page-c3c3a9be458adeed.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/p/[slug]/video/page-088684f252e3d59c.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/packages/page-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/page-6cf474454fb2ea5e.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/runs/page-69ee3fc638b1eecf.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/spend/page-201b626f17f11eeb.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/(app)/unsaved/page-c545489e991b83d2.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/_not-found/page-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/accounts/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/consult/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/context/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/hello/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/journal/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/keys/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/log/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/mcp/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/notes/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/tasks/[id]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/agent/tasks/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/ai/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/[operation]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/credentials/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/jobs/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/legal/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/[id]/settings/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/artifact/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/cancel/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/jobs/[jobId]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/apps/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/assets/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/assignments/launch/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/assignments/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/catalog/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/check/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/consultations/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/describe/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/disk/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/enrich/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/environment/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/hooks/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/ingest/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/apply/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/context/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/inspect/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/repair/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/md/review/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/memory/export/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/north/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/notes/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/open/all/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/open/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/project/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/rescan/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/roots/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/runs/[id]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/runs/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/scale/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/search/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/secrets/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/spend/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/tasks/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/today/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/assign/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/classify/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/critique/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/design/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/distill/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/episodes/learn/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/episodes/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/look/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/mine/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/rehearse/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/score/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/shot/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/sources/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/synthesize/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/taste/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/twin/verdicts/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/api/watch/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/global-not-found-69ee3fc638b1eecf.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/icon/[id]/route-de6e3e5d6198c075.js +0 -1
- package/app/apps/web/.next-bundle/static/chunks/app/not-found-69ee3fc638b1eecf.js +0 -1
- package/app/apps/web/.next-bundle/static/css/988bfb2024e770b7.css +0 -1
- package/app/apps/web/.next-bundle/static/mhsTm5XiqL98MCgDVB08u/_buildManifest.js +0 -1
- /package/app/apps/web/.next-bundle/static/{mhsTm5XiqL98MCgDVB08u → phRAJdip0Dtgh1L1Yw0jm}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,3876 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectCliAgents,
|
|
3
|
+
providersByAuth
|
|
4
|
+
} from "./chunk-6RVG2VUY.js";
|
|
5
|
+
import {
|
|
6
|
+
sinceMs
|
|
7
|
+
} from "./chunk-WAI2SCFY.js";
|
|
8
|
+
import {
|
|
9
|
+
catalogFetch,
|
|
10
|
+
unreachable
|
|
11
|
+
} from "./chunk-HHHT2MTQ.js";
|
|
12
|
+
import {
|
|
13
|
+
HandoffFault,
|
|
14
|
+
asHandoffFault,
|
|
15
|
+
handoffFaultText,
|
|
16
|
+
say
|
|
17
|
+
} from "./chunk-3QYJKRW6.js";
|
|
18
|
+
import "./chunk-DITTKJ7U.js";
|
|
19
|
+
import {
|
|
20
|
+
HANDOFF_CLAUDE_RECORD_VERSION,
|
|
21
|
+
HANDOFF_PROVENANCE_PREFIX,
|
|
22
|
+
REDACTED,
|
|
23
|
+
estimateTokens,
|
|
24
|
+
expandTilde,
|
|
25
|
+
panomaPath,
|
|
26
|
+
redactSecrets,
|
|
27
|
+
resolveExecutable
|
|
28
|
+
} from "./chunk-3T3WTSPS.js";
|
|
29
|
+
import "./chunk-MLKGABMK.js";
|
|
30
|
+
|
|
31
|
+
// src/handoff-command.ts
|
|
32
|
+
import { execFile } from "child_process";
|
|
33
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
34
|
+
import { resolve as resolve2 } from "path";
|
|
35
|
+
import { promisify } from "util";
|
|
36
|
+
import pc from "picocolors";
|
|
37
|
+
|
|
38
|
+
// ../../packages/handoff/dist/index.js
|
|
39
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
40
|
+
import { createHash } from "crypto";
|
|
41
|
+
import { existsSync } from "fs";
|
|
42
|
+
import { homedir } from "os";
|
|
43
|
+
import { posix, win32 } from "path";
|
|
44
|
+
import { readFile } from "fs/promises";
|
|
45
|
+
import { createHash as createHash2 } from "crypto";
|
|
46
|
+
import { readFileSync } from "fs";
|
|
47
|
+
import { realpath } from "fs/promises";
|
|
48
|
+
import { resolve } from "path";
|
|
49
|
+
import { readdir, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
50
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
51
|
+
import { createHash as createHash3 } from "crypto";
|
|
52
|
+
import { readFile as readFile4, stat as stat3 } from "fs/promises";
|
|
53
|
+
import { stat as stat4 } from "fs/promises";
|
|
54
|
+
import { readdir as readdir2, readFile as readFile5, stat as stat5 } from "fs/promises";
|
|
55
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
56
|
+
import { mkdir, rename, rm, writeFile } from "fs/promises";
|
|
57
|
+
import { appendFile } from "fs/promises";
|
|
58
|
+
import { open, readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
59
|
+
import { stat as stat7 } from "fs/promises";
|
|
60
|
+
var AGENT_IDS = [
|
|
61
|
+
"claude-cli",
|
|
62
|
+
"codex-cli",
|
|
63
|
+
"opencode",
|
|
64
|
+
"gemini-cli",
|
|
65
|
+
"cursor-agent",
|
|
66
|
+
"copilot-cli",
|
|
67
|
+
"aider",
|
|
68
|
+
"amp-cli",
|
|
69
|
+
"goose"
|
|
70
|
+
];
|
|
71
|
+
var NATIVE_AGENTS = ["claude-cli", "codex-cli", "opencode", "gemini-cli"];
|
|
72
|
+
var APP_OF = {
|
|
73
|
+
"claude-cli": { id: "claude-app", name: "Claude (app)", bundle: "Claude" },
|
|
74
|
+
"codex-cli": { id: "codex-app", name: "Codex (app)", bundle: "ChatGPT" }
|
|
75
|
+
};
|
|
76
|
+
var APP_WORDS = {
|
|
77
|
+
"claude-app": "claude-cli",
|
|
78
|
+
"codex-app": "codex-cli"
|
|
79
|
+
};
|
|
80
|
+
function agentOfApp(appId) {
|
|
81
|
+
return APP_WORDS[appId.trim().toLowerCase()];
|
|
82
|
+
}
|
|
83
|
+
var AGENT_WORDS = {
|
|
84
|
+
claude: "claude-cli",
|
|
85
|
+
codex: "codex-cli",
|
|
86
|
+
opencode: "opencode",
|
|
87
|
+
gemini: "gemini-cli",
|
|
88
|
+
cursor: "cursor-agent",
|
|
89
|
+
copilot: "copilot-cli",
|
|
90
|
+
aider: "aider",
|
|
91
|
+
amp: "amp-cli",
|
|
92
|
+
goose: "goose"
|
|
93
|
+
};
|
|
94
|
+
var AGENT_NAMES = {
|
|
95
|
+
"claude-cli": "Claude Code",
|
|
96
|
+
"codex-cli": "Codex CLI",
|
|
97
|
+
opencode: "OpenCode",
|
|
98
|
+
"gemini-cli": "Gemini CLI",
|
|
99
|
+
"cursor-agent": "Cursor Agent",
|
|
100
|
+
"copilot-cli": "GitHub Copilot",
|
|
101
|
+
aider: "Aider",
|
|
102
|
+
"amp-cli": "Amp",
|
|
103
|
+
goose: "Goose"
|
|
104
|
+
};
|
|
105
|
+
function agentFromWord(word) {
|
|
106
|
+
const lower = word.trim().toLowerCase();
|
|
107
|
+
if (AGENT_IDS.includes(lower)) return lower;
|
|
108
|
+
return AGENT_WORDS[lower];
|
|
109
|
+
}
|
|
110
|
+
function isAgentId(value) {
|
|
111
|
+
return AGENT_IDS.includes(value);
|
|
112
|
+
}
|
|
113
|
+
var NOTHING_DROPPED = Object.freeze({
|
|
114
|
+
thinking: 0,
|
|
115
|
+
images: 0,
|
|
116
|
+
subagents: 0,
|
|
117
|
+
offloaded: 0,
|
|
118
|
+
secrets: 0,
|
|
119
|
+
other: 0
|
|
120
|
+
});
|
|
121
|
+
var KEEP_TURNS_DEFAULT = 12;
|
|
122
|
+
var DISCOVERY_LIMIT_DEFAULT = 40;
|
|
123
|
+
var DISCOVERY_WHOLE_FILE_BYTES = 2 * 1024 * 1024;
|
|
124
|
+
var DISCOVERY_WINDOW_BYTES = 64 * 1024;
|
|
125
|
+
var LARGE_CONVERSATION_BYTES = 16 * 1024 * 1024;
|
|
126
|
+
var MAX_CONVERSATION_BYTES = 64 * 1024 * 1024;
|
|
127
|
+
var SAFE_ID = /^[A-Za-z0-9_-]{1,80}$/;
|
|
128
|
+
function isSafeId(value) {
|
|
129
|
+
return SAFE_ID.test(value);
|
|
130
|
+
}
|
|
131
|
+
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
132
|
+
var OPENCODE_SESSION = /^ses_[A-Za-z0-9]{20,40}$/;
|
|
133
|
+
function isSessionIdOf(agent, value) {
|
|
134
|
+
if (!isSafeId(value)) return false;
|
|
135
|
+
switch (agent) {
|
|
136
|
+
case "opencode":
|
|
137
|
+
return OPENCODE_SESSION.test(value);
|
|
138
|
+
case "claude-cli":
|
|
139
|
+
case "codex-cli":
|
|
140
|
+
case "gemini-cli":
|
|
141
|
+
return UUID.test(value);
|
|
142
|
+
default:
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function shortHandle(agent, sessionId) {
|
|
147
|
+
const body = agent === "opencode" && sessionId.startsWith("ses_") ? sessionId.slice(4) : sessionId;
|
|
148
|
+
return body.slice(0, 8);
|
|
149
|
+
}
|
|
150
|
+
function conversationId(agent, sessionId) {
|
|
151
|
+
return `${agent}:${sessionId}`;
|
|
152
|
+
}
|
|
153
|
+
function splitConversationId(id) {
|
|
154
|
+
const cut3 = id.indexOf(":");
|
|
155
|
+
if (cut3 <= 0 || cut3 === id.length - 1) return void 0;
|
|
156
|
+
return { agent: id.slice(0, cut3), sessionId: id.slice(cut3 + 1) };
|
|
157
|
+
}
|
|
158
|
+
var cryptoRandom = {
|
|
159
|
+
uuid: () => randomUUID(),
|
|
160
|
+
hex: (bytes) => randomBytes(bytes).toString("hex")
|
|
161
|
+
};
|
|
162
|
+
var BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
163
|
+
function opencodeId(prefix, at, counter, random) {
|
|
164
|
+
const ordered = (BigInt(at.getTime()) * 4096n + BigInt(counter % 4096)).toString(16).padStart(12, "0");
|
|
165
|
+
const tail = Array.from(Buffer.from(random.hex(14), "hex"), (byte) => BASE62[byte % 62]).join("");
|
|
166
|
+
return `${prefix}_${ordered.slice(-12)}${tail}`;
|
|
167
|
+
}
|
|
168
|
+
var NEVER = ["thinking and reasoning (never travels)", "images", "subagent runs"];
|
|
169
|
+
var FIDELITY = {
|
|
170
|
+
"claude-cli": {
|
|
171
|
+
agent: "claude-cli",
|
|
172
|
+
native: true,
|
|
173
|
+
carries: ["every message", "tool calls and their results, balanced", "the source's own summary", "the title"],
|
|
174
|
+
leaves: [...NEVER],
|
|
175
|
+
testedWith: "Claude Code 2.1.258 (11-Sep-2026)",
|
|
176
|
+
resumeShape: "claude --resume <id> finds the file from any folder"
|
|
177
|
+
},
|
|
178
|
+
"codex-cli": {
|
|
179
|
+
agent: "codex-cli",
|
|
180
|
+
native: true,
|
|
181
|
+
carries: ["every message", "tool calls and results as text notes", "the source's own summary", "the title as the preview"],
|
|
182
|
+
leaves: [...NEVER, "structured tool calls (Codex's own importer drops them too)"],
|
|
183
|
+
testedWith: "Codex CLI 0.153.0 (11-Sep-2026)",
|
|
184
|
+
resumeShape: "codex resume <id> from the project folder; the picker is scoped to it"
|
|
185
|
+
},
|
|
186
|
+
opencode: {
|
|
187
|
+
agent: "opencode",
|
|
188
|
+
native: true,
|
|
189
|
+
carries: ["every message", "tool calls and results as text notes", "the source's own summary as a compaction", "the title"],
|
|
190
|
+
leaves: [...NEVER, "the source's model: the next turn needs -m provider/model"],
|
|
191
|
+
testedWith: "OpenCode 1.2.6 import (11-Sep-2026)",
|
|
192
|
+
resumeShape: "opencode import <file>, then opencode -s <id>"
|
|
193
|
+
},
|
|
194
|
+
"gemini-cli": {
|
|
195
|
+
agent: "gemini-cli",
|
|
196
|
+
native: true,
|
|
197
|
+
carries: ["every message", "tool calls and results as text notes", "the source's own summary", "the title"],
|
|
198
|
+
leaves: [...NEVER, "structured tool calls"],
|
|
199
|
+
/*
|
|
200
|
+
No `testedWith`: the writer follows the CLI's source and was never run against a live
|
|
201
|
+
store (open-questions.md keeps the row). The field is data — a version and a date — or
|
|
202
|
+
absent; the screen words the absence in the viewer's language (`handoff.neverRunLive`)
|
|
203
|
+
instead of painting an English sentence from here, and the terminal and the channel never
|
|
204
|
+
printed this field. Changed 12-Sep-2026.
|
|
205
|
+
*/
|
|
206
|
+
resumeShape: "gemini --resume <id> from the project folder"
|
|
207
|
+
},
|
|
208
|
+
"cursor-agent": documentOnly("cursor-agent"),
|
|
209
|
+
"copilot-cli": documentOnly("copilot-cli"),
|
|
210
|
+
aider: documentOnly("aider"),
|
|
211
|
+
"amp-cli": documentOnly("amp-cli"),
|
|
212
|
+
goose: documentOnly("goose")
|
|
213
|
+
};
|
|
214
|
+
function documentOnly(agent) {
|
|
215
|
+
return {
|
|
216
|
+
agent,
|
|
217
|
+
native: false,
|
|
218
|
+
carries: ["a document with the digest and the last turns, to paste as the first message"],
|
|
219
|
+
leaves: [...NEVER, "the agent's own resume: it does not know this conversation"],
|
|
220
|
+
resumeShape: `${AGENT_NAMES[agent]} cannot resume a written conversation; it gets a document`
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function fidelityOf(target) {
|
|
224
|
+
return FIDELITY[target];
|
|
225
|
+
}
|
|
226
|
+
function isNativeTarget(target) {
|
|
227
|
+
return NATIVE_AGENTS.includes(target);
|
|
228
|
+
}
|
|
229
|
+
var RESUME_ARGV = {
|
|
230
|
+
"claude-cli": { command: "claude", args: (id) => ["--resume", id] },
|
|
231
|
+
"codex-cli": { command: "codex", args: (id) => ["resume", id] },
|
|
232
|
+
opencode: { command: "opencode", args: (id) => ["-s", id] },
|
|
233
|
+
"gemini-cli": { command: "gemini", args: (id) => ["--resume", id] }
|
|
234
|
+
};
|
|
235
|
+
var FORK_ARGV = {
|
|
236
|
+
"claude-cli": (id) => ["--resume", id, "--fork-session"],
|
|
237
|
+
"codex-cli": (id) => ["fork", id]
|
|
238
|
+
};
|
|
239
|
+
var SIGN_OUT = {
|
|
240
|
+
"claude-cli": "claude auth logout",
|
|
241
|
+
"codex-cli": "codex logout",
|
|
242
|
+
opencode: "opencode auth logout",
|
|
243
|
+
"gemini-cli": "/auth inside gemini"
|
|
244
|
+
};
|
|
245
|
+
var SIGN_IN = {
|
|
246
|
+
"claude-cli": "claude auth login",
|
|
247
|
+
"codex-cli": "codex login",
|
|
248
|
+
opencode: "opencode auth login",
|
|
249
|
+
"gemini-cli": "/auth inside gemini"
|
|
250
|
+
};
|
|
251
|
+
function quoteForShell(value, platform = process.platform) {
|
|
252
|
+
if (platform === "win32") return `'${value.replace(/'/g, "''")}'`;
|
|
253
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
254
|
+
}
|
|
255
|
+
function resumeOf(agent, sessionId, cwd, platform = process.platform) {
|
|
256
|
+
const entry = RESUME_ARGV[agent];
|
|
257
|
+
if (!entry || !isSessionIdOf(agent, sessionId)) return void 0;
|
|
258
|
+
const args = entry.args(sessionId);
|
|
259
|
+
return { command: entry.command, args, line: line(entry.command, args, cwd, platform) };
|
|
260
|
+
}
|
|
261
|
+
function forkOf(agent, sessionId, cwd, platform = process.platform) {
|
|
262
|
+
const entry = RESUME_ARGV[agent];
|
|
263
|
+
const fork = FORK_ARGV[agent];
|
|
264
|
+
if (!entry || !fork || !isSessionIdOf(agent, sessionId)) return void 0;
|
|
265
|
+
const args = fork(sessionId);
|
|
266
|
+
return { command: entry.command, args, line: line(entry.command, args, cwd, platform) };
|
|
267
|
+
}
|
|
268
|
+
function line(command, args, cwd, platform) {
|
|
269
|
+
const call = [command, ...args].join(" ");
|
|
270
|
+
if (platform === "win32") return `Set-Location -LiteralPath ${quoteForShell(cwd, platform)}; ${call}`;
|
|
271
|
+
return `cd ${quoteForShell(cwd, platform)} && ${call}`;
|
|
272
|
+
}
|
|
273
|
+
var APP_URL = {
|
|
274
|
+
"claude-cli": (id) => `claude://resume?session=${id}`,
|
|
275
|
+
"codex-cli": (id) => `codex://threads/${id}`
|
|
276
|
+
};
|
|
277
|
+
function resumeInApp(agent, sessionId, cwd, platform = process.platform) {
|
|
278
|
+
const app = APP_OF[agent];
|
|
279
|
+
const url = APP_URL[agent];
|
|
280
|
+
if (!app || !url || platform !== "darwin" || !isSessionIdOf(agent, sessionId)) return void 0;
|
|
281
|
+
const link = url(sessionId);
|
|
282
|
+
return {
|
|
283
|
+
app,
|
|
284
|
+
url: link,
|
|
285
|
+
line: `open ${quoteForShell(link, platform)}`,
|
|
286
|
+
sentence: agent === "claude-cli" ? `Open Claude, the Code tab, the folder ${cwd}, and pick the conversation ${sessionId}` : `Open the Codex app, the folder ${cwd}, and pick the thread ${sessionId}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
var TEST_GUARD_DETAIL = "tests must pass home and env";
|
|
290
|
+
function resolveStoreOptions(options = {}) {
|
|
291
|
+
if (process.env.NODE_ENV === "test" && (options.home === void 0 || options.env === void 0)) {
|
|
292
|
+
throw new HandoffFault("store-missing", TEST_GUARD_DETAIL);
|
|
293
|
+
}
|
|
294
|
+
const platform = options.platform ?? process.platform;
|
|
295
|
+
return {
|
|
296
|
+
home: options.home ?? homedir(),
|
|
297
|
+
env: options.env ?? process.env,
|
|
298
|
+
platform,
|
|
299
|
+
path: platform === "win32" ? win32 : posix
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function envValue(env, name) {
|
|
303
|
+
const value = env[name]?.trim();
|
|
304
|
+
return value ? value : void 0;
|
|
305
|
+
}
|
|
306
|
+
function exists(path) {
|
|
307
|
+
try {
|
|
308
|
+
return existsSync(path);
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function nativePath() {
|
|
314
|
+
return process.platform === "win32" ? win32 : posix;
|
|
315
|
+
}
|
|
316
|
+
function isAbsoluteOn(path, platform) {
|
|
317
|
+
return platform === "win32" ? win32.isAbsolute(path) : posix.isAbsolute(path);
|
|
318
|
+
}
|
|
319
|
+
var MAX_SLUG = 200;
|
|
320
|
+
function claudeStore(options = {}) {
|
|
321
|
+
const resolved = resolveStoreOptions(options);
|
|
322
|
+
const { path } = resolved;
|
|
323
|
+
const root = envValue(resolved.env, "CLAUDE_CONFIG_DIR") ?? path.join(resolved.home, ".claude");
|
|
324
|
+
return { root, projects: path.join(root, "projects"), resolved };
|
|
325
|
+
}
|
|
326
|
+
function claudeStoreAt(root, options = {}) {
|
|
327
|
+
const resolved = resolveStoreOptions(options);
|
|
328
|
+
return { root, projects: resolved.path.join(root, "projects"), resolved };
|
|
329
|
+
}
|
|
330
|
+
function claudeStoreExists(store) {
|
|
331
|
+
return exists(store.projects);
|
|
332
|
+
}
|
|
333
|
+
function claudeSlug(cwd) {
|
|
334
|
+
const slug = cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
335
|
+
if (slug.length <= MAX_SLUG) return slug;
|
|
336
|
+
const hash = createHash("sha256").update(cwd).digest("hex").slice(0, 8);
|
|
337
|
+
return `${slug.slice(0, MAX_SLUG - hash.length - 1)}-${hash}`;
|
|
338
|
+
}
|
|
339
|
+
function claudeFolderHolds(name, cwd, platform = process.platform) {
|
|
340
|
+
const fold = (s) => platform === "win32" ? s.toLowerCase() : s;
|
|
341
|
+
const own = fold(claudeSlug(cwd.replace(/[\\/]+$/, "")));
|
|
342
|
+
const child = `${own}-`.slice(0, MAX_SLUG - 9);
|
|
343
|
+
if (fold(name) === own) return "own";
|
|
344
|
+
return fold(name).startsWith(child) ? "maybe" : false;
|
|
345
|
+
}
|
|
346
|
+
function claudeProjectDir(store, cwd) {
|
|
347
|
+
return store.resolved.path.join(store.projects, claudeSlug(cwd));
|
|
348
|
+
}
|
|
349
|
+
function claudeSidecarDir(transcriptPath, path = nativePath()) {
|
|
350
|
+
const dir = path.dirname(transcriptPath);
|
|
351
|
+
const id = path.basename(transcriptPath, ".jsonl");
|
|
352
|
+
return path.join(dir, id);
|
|
353
|
+
}
|
|
354
|
+
var CODEX_DEFAULT_MODEL = "gpt-5-codex";
|
|
355
|
+
var CODEX_DEFAULT_REASONING_EFFORT = "medium";
|
|
356
|
+
var CODEX_DEFAULT_APPROVAL_POLICY = "on-request";
|
|
357
|
+
var CODEX_DEFAULT_SANDBOX_MODE = "workspace-write";
|
|
358
|
+
function build(root, resolved) {
|
|
359
|
+
const { path } = resolved;
|
|
360
|
+
return {
|
|
361
|
+
root,
|
|
362
|
+
sessions: path.join(root, "sessions"),
|
|
363
|
+
index: path.join(root, "session_index.jsonl"),
|
|
364
|
+
config: path.join(root, "config.toml"),
|
|
365
|
+
resolved
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function codexStore(options = {}) {
|
|
369
|
+
const resolved = resolveStoreOptions(options);
|
|
370
|
+
const root = envValue(resolved.env, "CODEX_HOME") ?? resolved.path.join(resolved.home, ".codex");
|
|
371
|
+
return build(root, resolved);
|
|
372
|
+
}
|
|
373
|
+
function codexStoreAt(root, options = {}) {
|
|
374
|
+
return build(root, resolveStoreOptions(options));
|
|
375
|
+
}
|
|
376
|
+
function codexStoreExists(store) {
|
|
377
|
+
return exists(store.sessions);
|
|
378
|
+
}
|
|
379
|
+
function localFields(at) {
|
|
380
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
381
|
+
return [String(at.getFullYear()).padStart(4, "0"), two(at.getMonth() + 1), two(at.getDate()), two(at.getHours()), two(at.getMinutes()), two(at.getSeconds())];
|
|
382
|
+
}
|
|
383
|
+
function codexFileStamp(at) {
|
|
384
|
+
const [year, month, day, hour, minute, second] = localFields(at);
|
|
385
|
+
return `${year}-${month}-${day}T${hour}-${minute}-${second}`;
|
|
386
|
+
}
|
|
387
|
+
function codexRolloutPath(store, at, sessionId) {
|
|
388
|
+
const { path } = store.resolved;
|
|
389
|
+
const [year, month, day] = localFields(at);
|
|
390
|
+
return path.join(store.sessions, year, month, day, `rollout-${codexFileStamp(at)}-${sessionId}.jsonl`);
|
|
391
|
+
}
|
|
392
|
+
async function codexModelSettings(store) {
|
|
393
|
+
const settings = {
|
|
394
|
+
model: CODEX_DEFAULT_MODEL,
|
|
395
|
+
reasoningEffort: CODEX_DEFAULT_REASONING_EFFORT,
|
|
396
|
+
approvalPolicy: CODEX_DEFAULT_APPROVAL_POLICY,
|
|
397
|
+
sandboxMode: CODEX_DEFAULT_SANDBOX_MODE
|
|
398
|
+
};
|
|
399
|
+
let text;
|
|
400
|
+
try {
|
|
401
|
+
text = await readFile(store.config, "utf8");
|
|
402
|
+
} catch {
|
|
403
|
+
return settings;
|
|
404
|
+
}
|
|
405
|
+
const firstTable = /^[ \t]*\[/m.exec(text);
|
|
406
|
+
const top = firstTable ? text.slice(0, firstTable.index) : text;
|
|
407
|
+
const line2 = (key) => new RegExp(`^[ \\t]*${key}[ \\t]*=[ \\t]*"([^"\\n]+)"`, "m").exec(top)?.[1]?.trim();
|
|
408
|
+
const model = line2("model");
|
|
409
|
+
const effort = line2("model_reasoning_effort");
|
|
410
|
+
const approval = line2("approval_policy");
|
|
411
|
+
const sandbox = line2("sandbox_mode");
|
|
412
|
+
if (model) settings.model = model;
|
|
413
|
+
if (effort) settings.reasoningEffort = effort;
|
|
414
|
+
if (approval) settings.approvalPolicy = approval;
|
|
415
|
+
if (sandbox) settings.sandboxMode = sandbox;
|
|
416
|
+
return settings;
|
|
417
|
+
}
|
|
418
|
+
function codexIdFromName(name) {
|
|
419
|
+
const match = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i.exec(
|
|
420
|
+
name
|
|
421
|
+
);
|
|
422
|
+
return match?.[1];
|
|
423
|
+
}
|
|
424
|
+
function build2(root, resolved) {
|
|
425
|
+
const { path } = resolved;
|
|
426
|
+
return { root, db: path.join(root, "opencode.db"), storage: path.join(root, "storage"), resolved };
|
|
427
|
+
}
|
|
428
|
+
function opencodeStore(options = {}) {
|
|
429
|
+
const resolved = resolveStoreOptions(options);
|
|
430
|
+
const { path, env, home, platform } = resolved;
|
|
431
|
+
const xdg = envValue(env, "XDG_DATA_HOME");
|
|
432
|
+
const data = xdg ?? (platform === "win32" ? envValue(env, "LOCALAPPDATA") ?? path.join(home, "AppData", "Local") : path.join(home, ".local", "share"));
|
|
433
|
+
return build2(path.join(data, "opencode"), resolved);
|
|
434
|
+
}
|
|
435
|
+
function opencodeStoreAt(root, options = {}) {
|
|
436
|
+
return build2(root, resolveStoreOptions(options));
|
|
437
|
+
}
|
|
438
|
+
function opencodeStoreExists(store) {
|
|
439
|
+
return exists(store.db) || exists(store.resolved.path.join(store.storage, "session"));
|
|
440
|
+
}
|
|
441
|
+
function opencodeEnvelopePath(store, sessionId) {
|
|
442
|
+
return store.resolved.path.join(store.root, `panoma-import-${sessionId}.json`);
|
|
443
|
+
}
|
|
444
|
+
var ENVELOPE_NAME = /^panoma-import-(ses_[A-Za-z0-9]+)\.json$/;
|
|
445
|
+
function opencodeEnvelopeId(name) {
|
|
446
|
+
return ENVELOPE_NAME.exec(name)?.[1];
|
|
447
|
+
}
|
|
448
|
+
async function realFolder(folder) {
|
|
449
|
+
if (!folder) return folder;
|
|
450
|
+
const absolute = resolve(folder);
|
|
451
|
+
return realpath(absolute).catch(() => absolute);
|
|
452
|
+
}
|
|
453
|
+
function flat(path, platform) {
|
|
454
|
+
const folded = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
455
|
+
return platform === "win32" ? folded.toLowerCase() : folded;
|
|
456
|
+
}
|
|
457
|
+
function insideFolder(cwd, folder, platform = process.platform) {
|
|
458
|
+
if (!cwd) return false;
|
|
459
|
+
const a = flat(cwd, platform);
|
|
460
|
+
const b = flat(folder, platform);
|
|
461
|
+
return a === b || a.startsWith(`${b}/`);
|
|
462
|
+
}
|
|
463
|
+
function sameFolder(a, b, platform = process.platform) {
|
|
464
|
+
if (!a || !b) return false;
|
|
465
|
+
return flat(a, platform) === flat(b, platform);
|
|
466
|
+
}
|
|
467
|
+
function build3(root, resolved) {
|
|
468
|
+
const { path } = resolved;
|
|
469
|
+
return { root, tmp: path.join(root, "tmp"), registry: path.join(root, "projects.json"), resolved };
|
|
470
|
+
}
|
|
471
|
+
function geminiStore(options = {}) {
|
|
472
|
+
const resolved = resolveStoreOptions(options);
|
|
473
|
+
return build3(resolved.path.join(resolved.home, ".gemini"), resolved);
|
|
474
|
+
}
|
|
475
|
+
function geminiStoreAt(root, options = {}) {
|
|
476
|
+
return build3(root, resolveStoreOptions(options));
|
|
477
|
+
}
|
|
478
|
+
function geminiStoreExists(store) {
|
|
479
|
+
return exists(store.tmp);
|
|
480
|
+
}
|
|
481
|
+
function sha256Hex(text) {
|
|
482
|
+
return createHash2("sha256").update(text).digest("hex");
|
|
483
|
+
}
|
|
484
|
+
function readGeminiRegistry(store) {
|
|
485
|
+
let raw;
|
|
486
|
+
try {
|
|
487
|
+
raw = readFileSync(store.registry, "utf8");
|
|
488
|
+
} catch {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
let parsed;
|
|
492
|
+
try {
|
|
493
|
+
parsed = JSON.parse(raw);
|
|
494
|
+
} catch {
|
|
495
|
+
return void 0;
|
|
496
|
+
}
|
|
497
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
498
|
+
const record2 = parsed;
|
|
499
|
+
const projects = typeof record2["projects"] === "object" && record2["projects"] !== null ? record2["projects"] : record2;
|
|
500
|
+
const map = /* @__PURE__ */ new Map();
|
|
501
|
+
for (const [folder, slug] of Object.entries(projects)) {
|
|
502
|
+
if (typeof slug === "string" && slug.length > 0) map.set(folder.replace(/[\\/]+$/, ""), slug);
|
|
503
|
+
}
|
|
504
|
+
return map;
|
|
505
|
+
}
|
|
506
|
+
function geminiProjectId(store, cwd) {
|
|
507
|
+
const registry = readGeminiRegistry(store);
|
|
508
|
+
if (registry) {
|
|
509
|
+
for (const [folder, slug] of registry) {
|
|
510
|
+
if (sameFolder(folder, cwd, store.resolved.platform)) return slug;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return sha256Hex(cwd);
|
|
514
|
+
}
|
|
515
|
+
function geminiProjectFolders(store, candidates) {
|
|
516
|
+
const folders = /* @__PURE__ */ new Map();
|
|
517
|
+
const registry = readGeminiRegistry(store);
|
|
518
|
+
if (registry) for (const [folder, slug] of registry) folders.set(slug, folder);
|
|
519
|
+
for (const cwd of candidates) folders.set(sha256Hex(cwd), cwd);
|
|
520
|
+
return folders;
|
|
521
|
+
}
|
|
522
|
+
function geminiChatsDir(store, projectId) {
|
|
523
|
+
return store.resolved.path.join(store.tmp, projectId, "chats");
|
|
524
|
+
}
|
|
525
|
+
function geminiChatPath(store, projectId, at, sessionId) {
|
|
526
|
+
const stamp = at.toISOString().slice(0, 16).replace(/:/g, "-");
|
|
527
|
+
return store.resolved.path.join(geminiChatsDir(store, projectId), `session-${stamp}-${sessionId.slice(0, 8)}.jsonl`);
|
|
528
|
+
}
|
|
529
|
+
var NOTE_INPUT_CHARS = 8e3;
|
|
530
|
+
var NOTE_OUTPUT_CHARS = 16e3;
|
|
531
|
+
function noteOfCall(part) {
|
|
532
|
+
return `[tool call: ${part.name}]
|
|
533
|
+
${renderInput(part.input)}`;
|
|
534
|
+
}
|
|
535
|
+
function noteOfResult(part) {
|
|
536
|
+
const head = part.isError ? "[tool result: error]" : "[tool result]";
|
|
537
|
+
return `${head}
|
|
538
|
+
${cut(part.output, NOTE_OUTPUT_CHARS)}`;
|
|
539
|
+
}
|
|
540
|
+
function textOfPart(part) {
|
|
541
|
+
switch (part.kind) {
|
|
542
|
+
case "text":
|
|
543
|
+
case "summary":
|
|
544
|
+
return part.text;
|
|
545
|
+
case "tool_call":
|
|
546
|
+
return noteOfCall(part);
|
|
547
|
+
case "tool_result":
|
|
548
|
+
return noteOfResult(part);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function renderInput(input) {
|
|
552
|
+
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
553
|
+
const lines = [];
|
|
554
|
+
for (const [key, value] of Object.entries(input)) {
|
|
555
|
+
const text = typeof value === "string" ? value : stringify(value);
|
|
556
|
+
lines.push(`${key}: ${cut(text, NOTE_INPUT_CHARS)}`);
|
|
557
|
+
}
|
|
558
|
+
return lines.join("\n");
|
|
559
|
+
}
|
|
560
|
+
if (typeof input === "string") return cut(input, NOTE_INPUT_CHARS);
|
|
561
|
+
return cut(stringify(input), NOTE_INPUT_CHARS);
|
|
562
|
+
}
|
|
563
|
+
function stringify(value) {
|
|
564
|
+
try {
|
|
565
|
+
return JSON.stringify(value) ?? "";
|
|
566
|
+
} catch {
|
|
567
|
+
return "";
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function cut(text, max) {
|
|
571
|
+
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
572
|
+
}
|
|
573
|
+
function provenanceLine(source, sessionId, tier, at) {
|
|
574
|
+
const date = at.toISOString().slice(0, 10);
|
|
575
|
+
return `${HANDOFF_PROVENANCE_PREFIX}${AGENT_NAMES[source]} conversation ${sessionId} by panoma on ${date} \xB7 tier ${tier}`;
|
|
576
|
+
}
|
|
577
|
+
function stripProvenance(text) {
|
|
578
|
+
if (!text.startsWith(HANDOFF_PROVENANCE_PREFIX)) return text;
|
|
579
|
+
const cut3 = text.indexOf("\n");
|
|
580
|
+
if (cut3 === -1) return "";
|
|
581
|
+
return text.slice(cut3 + 1).replace(/^\n+/, "");
|
|
582
|
+
}
|
|
583
|
+
function normalizeTurns(turns) {
|
|
584
|
+
const out = [];
|
|
585
|
+
for (const turn of turns) {
|
|
586
|
+
const parts = turn.parts.map(textOfPart).filter((text) => text.length > 0);
|
|
587
|
+
const last = out[out.length - 1];
|
|
588
|
+
if (last && last.role === turn.role) last.parts.push(...parts);
|
|
589
|
+
else out.push({ role: turn.role, parts });
|
|
590
|
+
}
|
|
591
|
+
return out.filter((turn) => turn.parts.length > 0);
|
|
592
|
+
}
|
|
593
|
+
function hashTurns(turns) {
|
|
594
|
+
return createHash3("sha256").update(JSON.stringify(normalizeTurns(turns))).digest("hex");
|
|
595
|
+
}
|
|
596
|
+
function isRecord(value) {
|
|
597
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
598
|
+
}
|
|
599
|
+
function readString(value) {
|
|
600
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
601
|
+
}
|
|
602
|
+
function readNumber(value) {
|
|
603
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
604
|
+
}
|
|
605
|
+
function emptyDropped() {
|
|
606
|
+
return { thinking: 0, images: 0, subagents: 0, offloaded: 0, secrets: 0, other: 0 };
|
|
607
|
+
}
|
|
608
|
+
function isoFromEpochSeconds(value) {
|
|
609
|
+
const seconds = readNumber(value);
|
|
610
|
+
if (seconds === void 0 || seconds <= 0) return void 0;
|
|
611
|
+
const ms = seconds > 1e12 ? seconds : seconds * 1e3;
|
|
612
|
+
return new Date(ms).toISOString();
|
|
613
|
+
}
|
|
614
|
+
function isoFromEpochMs(value) {
|
|
615
|
+
const ms = readNumber(value);
|
|
616
|
+
return ms === void 0 || ms <= 0 ? void 0 : new Date(ms).toISOString();
|
|
617
|
+
}
|
|
618
|
+
var TurnBuilder = class {
|
|
619
|
+
turns = [];
|
|
620
|
+
push(role, part, at) {
|
|
621
|
+
const last = this.turns[this.turns.length - 1];
|
|
622
|
+
if (last && last.role === role && !last.parts.some((p) => p.kind === "summary") && part.kind !== "summary") {
|
|
623
|
+
last.parts.push(part);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const turn = { role, parts: [part] };
|
|
627
|
+
if (at) turn.at = at;
|
|
628
|
+
this.turns.push(turn);
|
|
629
|
+
}
|
|
630
|
+
/** A new turn even when the role repeats: two user prompts in a row stay two turns. */
|
|
631
|
+
open(role, parts, at) {
|
|
632
|
+
if (parts.length === 0) return;
|
|
633
|
+
const turn = { role, parts };
|
|
634
|
+
if (at) turn.at = at;
|
|
635
|
+
this.turns.push(turn);
|
|
636
|
+
}
|
|
637
|
+
/** Everything read so far is behind a compaction: the turns start again. */
|
|
638
|
+
reset() {
|
|
639
|
+
this.turns.length = 0;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
async function readTranscript(path) {
|
|
643
|
+
let size2;
|
|
644
|
+
try {
|
|
645
|
+
size2 = (await stat(path)).size;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
throw asHandoffFault(error, "conversation-not-found");
|
|
648
|
+
}
|
|
649
|
+
if (size2 > MAX_CONVERSATION_BYTES) throw new HandoffFault("too-large", String(size2));
|
|
650
|
+
try {
|
|
651
|
+
return await readFile2(path, "utf8");
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw asHandoffFault(error, "unreadable-transcript");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function parseLines(text) {
|
|
657
|
+
const out = [];
|
|
658
|
+
for (const line2 of text.split("\n")) {
|
|
659
|
+
const trimmed = line2.trim();
|
|
660
|
+
if (trimmed.length < 2) continue;
|
|
661
|
+
try {
|
|
662
|
+
out.push(JSON.parse(trimmed));
|
|
663
|
+
} catch {
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function finishConversation(head, turns, compactions, dropped) {
|
|
669
|
+
const first = turns[0]?.parts[0];
|
|
670
|
+
if (first && (first.kind === "text" || first.kind === "summary")) {
|
|
671
|
+
first.text = stripProvenance(first.text);
|
|
672
|
+
if (first.text.length === 0) {
|
|
673
|
+
turns[0].parts.shift();
|
|
674
|
+
if (turns[0].parts.length === 0) turns.shift();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const ref = {
|
|
678
|
+
id: conversationId(head.agent, head.sessionId),
|
|
679
|
+
agent: head.agent,
|
|
680
|
+
sessionId: head.sessionId,
|
|
681
|
+
handle: shortHandle(head.agent, head.sessionId),
|
|
682
|
+
path: head.path,
|
|
683
|
+
cwd: head.cwd,
|
|
684
|
+
updatedAt: head.updatedAt,
|
|
685
|
+
turnCount: turns.length,
|
|
686
|
+
bytes: head.bytes,
|
|
687
|
+
compacted: head.compacted || compactions.length > 0
|
|
688
|
+
};
|
|
689
|
+
if (head.gitBranch) ref.gitBranch = head.gitBranch;
|
|
690
|
+
if (head.title) ref.title = head.title;
|
|
691
|
+
if (head.model) ref.model = head.model;
|
|
692
|
+
if (head.startedAt) ref.startedAt = head.startedAt;
|
|
693
|
+
if (head.limit) ref.limit = head.limit;
|
|
694
|
+
if (head.surface) ref.surface = head.surface;
|
|
695
|
+
return { ...ref, version: 1, hash: hashTurns(turns), turns, compactions, dropped };
|
|
696
|
+
}
|
|
697
|
+
function titleFromText(text, max = 80) {
|
|
698
|
+
const line2 = text.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
699
|
+
if (!line2) return void 0;
|
|
700
|
+
return line2.length > max ? `${line2.slice(0, max - 1).trimEnd()}\u2026` : line2;
|
|
701
|
+
}
|
|
702
|
+
var APP_ENTRYPOINTS = ["claude-desktop", "claude-desktop-3p"];
|
|
703
|
+
function surfaceOfEntrypoint(entrypoint) {
|
|
704
|
+
return entrypoint !== void 0 && APP_ENTRYPOINTS.includes(entrypoint) ? "app" : "cli";
|
|
705
|
+
}
|
|
706
|
+
var OFFLOADED_MAX_BYTES = DISCOVERY_WINDOW_BYTES;
|
|
707
|
+
var SAVED_TO = /Full output saved to: (.+?)(?:\r?\n|$)/;
|
|
708
|
+
var SYNTHETIC_MODEL = "<synthetic>";
|
|
709
|
+
var INJECTED_BLOCKS = [
|
|
710
|
+
/<system-reminder>[\s\S]*?<\/system-reminder>/g,
|
|
711
|
+
/<task-notification>[\s\S]*?<\/task-notification>/g,
|
|
712
|
+
/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g,
|
|
713
|
+
/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g,
|
|
714
|
+
/<local-command-stderr>[\s\S]*?<\/local-command-stderr>/g,
|
|
715
|
+
/<command-name>[\s\S]*?<\/command-name>/g,
|
|
716
|
+
/<command-message>[\s\S]*?<\/command-message>/g,
|
|
717
|
+
/<command-args>[\s\S]*?<\/command-args>/g
|
|
718
|
+
];
|
|
719
|
+
var INJECTED_LINES = [/^\[Request interrupted[^\n]*$/gm, /^Caveat: The messages below[^\n]*$/gm];
|
|
720
|
+
function stripClaudeInjections(text) {
|
|
721
|
+
let out = text;
|
|
722
|
+
for (const block of INJECTED_BLOCKS) out = out.replace(block, " ");
|
|
723
|
+
for (const line2 of INJECTED_LINES) out = out.replace(line2, " ");
|
|
724
|
+
return out === text ? text : out.trim();
|
|
725
|
+
}
|
|
726
|
+
async function readClaudeConversation(path, options = {}) {
|
|
727
|
+
resolveStoreOptions(options);
|
|
728
|
+
const text = await readTranscript(path);
|
|
729
|
+
const local = nativePath();
|
|
730
|
+
const info = await stat2(path);
|
|
731
|
+
return parseClaudeTranscript(text, {
|
|
732
|
+
path,
|
|
733
|
+
sessionId: local.basename(path, ".jsonl"),
|
|
734
|
+
bytes: info.size,
|
|
735
|
+
mtime: info.mtime.toISOString(),
|
|
736
|
+
sidecar: claudeSidecarDir(path, local)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
async function parseClaudeTranscript(text, input) {
|
|
740
|
+
const local = nativePath();
|
|
741
|
+
const { sidecar, sessionId } = input;
|
|
742
|
+
const builder = new TurnBuilder();
|
|
743
|
+
const compactions = [];
|
|
744
|
+
const dropped = emptyDropped();
|
|
745
|
+
const head = {
|
|
746
|
+
agent: "claude-cli",
|
|
747
|
+
sessionId,
|
|
748
|
+
path: input.path,
|
|
749
|
+
cwd: "",
|
|
750
|
+
updatedAt: input.mtime,
|
|
751
|
+
bytes: input.bytes,
|
|
752
|
+
compacted: false,
|
|
753
|
+
surface: "cli"
|
|
754
|
+
};
|
|
755
|
+
let entrypoint;
|
|
756
|
+
let customTitle;
|
|
757
|
+
let aiTitle;
|
|
758
|
+
let firstUserText;
|
|
759
|
+
let pending;
|
|
760
|
+
let limit;
|
|
761
|
+
let lastAt;
|
|
762
|
+
for (const record2 of parseLines(text)) {
|
|
763
|
+
if (!isRecord(record2)) continue;
|
|
764
|
+
const type = record2["type"];
|
|
765
|
+
const at = readString(record2["timestamp"]);
|
|
766
|
+
if (entrypoint === void 0) {
|
|
767
|
+
entrypoint = readString(record2["entrypoint"]);
|
|
768
|
+
if (entrypoint !== void 0) head.surface = surfaceOfEntrypoint(entrypoint);
|
|
769
|
+
}
|
|
770
|
+
if (type === "custom-title") {
|
|
771
|
+
customTitle = readString(record2["customTitle"]) ?? customTitle;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (type === "ai-title") {
|
|
775
|
+
aiTitle = readString(record2["aiTitle"]) ?? aiTitle;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (type === "system") {
|
|
779
|
+
if (record2["subtype"] !== "compact_boundary") continue;
|
|
780
|
+
builder.reset();
|
|
781
|
+
compactions.length = 0;
|
|
782
|
+
head.compacted = true;
|
|
783
|
+
firstUserText = void 0;
|
|
784
|
+
const meta = record2["compactMetadata"];
|
|
785
|
+
pending = { at, tokensBefore: isRecord(meta) ? readNumber(meta["preTokens"]) : void 0 };
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (type !== "user" && type !== "assistant") continue;
|
|
789
|
+
if (at) {
|
|
790
|
+
head.startedAt ??= at;
|
|
791
|
+
lastAt = at;
|
|
792
|
+
}
|
|
793
|
+
const cwd = readString(record2["cwd"]);
|
|
794
|
+
if (cwd && !head.cwd) head.cwd = cwd;
|
|
795
|
+
const branch = readString(record2["gitBranch"]);
|
|
796
|
+
if (branch) head.gitBranch = branch;
|
|
797
|
+
if (record2["isSidechain"] === true) {
|
|
798
|
+
dropped.other += 1;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const message = record2["message"];
|
|
802
|
+
if (!isRecord(message)) continue;
|
|
803
|
+
if (type === "user") {
|
|
804
|
+
if (record2["isMeta"] === true) {
|
|
805
|
+
dropped.other += 1;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (record2["isCompactSummary"] === true) {
|
|
809
|
+
const summary = contentText(message["content"]);
|
|
810
|
+
builder.open("user", [{ kind: "summary", text: summary }], at);
|
|
811
|
+
const compaction = { text: summary };
|
|
812
|
+
const when = pending?.at ?? at;
|
|
813
|
+
if (when) compaction.at = when;
|
|
814
|
+
if (pending?.tokensBefore !== void 0) compaction.tokensBefore = pending.tokensBefore;
|
|
815
|
+
compactions.push(compaction);
|
|
816
|
+
pending = void 0;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
const parts = await userParts(message["content"], sidecar, dropped, local);
|
|
820
|
+
if (firstUserText === void 0) {
|
|
821
|
+
const first = parts.find((p) => p.kind === "text");
|
|
822
|
+
if (first && first.kind === "text") firstUserText = first.text;
|
|
823
|
+
}
|
|
824
|
+
for (const part of parts) builder.push("user", part, at);
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (record2["isApiErrorMessage"] === true) {
|
|
828
|
+
limit = limitOf(record2, at) ?? limit;
|
|
829
|
+
dropped.other += 1;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
limit = void 0;
|
|
833
|
+
const model = readString(message["model"]);
|
|
834
|
+
if (model && model !== SYNTHETIC_MODEL && !head.model) head.model = model;
|
|
835
|
+
for (const part of assistantParts(message["content"], dropped)) builder.push("assistant", part, at);
|
|
836
|
+
}
|
|
837
|
+
if (lastAt) head.updatedAt = lastAt;
|
|
838
|
+
head.title = customTitle ?? aiTitle ?? (firstUserText ? titleFromText(firstUserText) : void 0);
|
|
839
|
+
if (limit) head.limit = limit;
|
|
840
|
+
if (sidecar) dropped.subagents += await countSubagents(local.join(sidecar, "subagents"), local);
|
|
841
|
+
return finishConversation(head, builder.turns, compactions, dropped);
|
|
842
|
+
}
|
|
843
|
+
function limitOf(record2, at) {
|
|
844
|
+
const quota = record2["quotaLimits"];
|
|
845
|
+
const status = readNumber(record2["apiErrorStatus"]);
|
|
846
|
+
if (status !== 429 && !isRecord(quota)) return void 0;
|
|
847
|
+
const hit = { at: at ?? "" };
|
|
848
|
+
if (isRecord(quota)) {
|
|
849
|
+
const resetsAt = isoFromEpochSeconds(quota["resetsAt"]);
|
|
850
|
+
if (resetsAt) hit.resetsAt = resetsAt;
|
|
851
|
+
const kind = readString(quota["rateLimitType"]);
|
|
852
|
+
if (kind) hit.kind = kind;
|
|
853
|
+
}
|
|
854
|
+
return hit;
|
|
855
|
+
}
|
|
856
|
+
async function userParts(content, sidecar, dropped, path) {
|
|
857
|
+
const parts = [];
|
|
858
|
+
const own = (text) => {
|
|
859
|
+
const words = stripClaudeInjections(text);
|
|
860
|
+
if (words) parts.push({ kind: "text", text: words });
|
|
861
|
+
else dropped.other += 1;
|
|
862
|
+
};
|
|
863
|
+
if (typeof content === "string") {
|
|
864
|
+
if (content.length > 0) own(content);
|
|
865
|
+
return parts;
|
|
866
|
+
}
|
|
867
|
+
if (!Array.isArray(content)) return [];
|
|
868
|
+
for (const block of content) {
|
|
869
|
+
if (!isRecord(block)) continue;
|
|
870
|
+
const kind = block["type"];
|
|
871
|
+
if (kind === "text") {
|
|
872
|
+
const text = readString(block["text"]);
|
|
873
|
+
if (text) own(text);
|
|
874
|
+
} else if (kind === "tool_result") {
|
|
875
|
+
const callId = readString(block["tool_use_id"]);
|
|
876
|
+
if (!callId) continue;
|
|
877
|
+
let output = contentText(block["content"], dropped);
|
|
878
|
+
output = await inlineOffloaded(output, sidecar, dropped, path);
|
|
879
|
+
const part = { kind: "tool_result", callId, output };
|
|
880
|
+
if (block["is_error"] === true) part.isError = true;
|
|
881
|
+
parts.push(part);
|
|
882
|
+
} else if (kind === "image") {
|
|
883
|
+
dropped.images += 1;
|
|
884
|
+
} else {
|
|
885
|
+
dropped.other += 1;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return parts;
|
|
889
|
+
}
|
|
890
|
+
function assistantParts(content, dropped) {
|
|
891
|
+
if (typeof content === "string") return content.length > 0 ? [{ kind: "text", text: content }] : [];
|
|
892
|
+
if (!Array.isArray(content)) return [];
|
|
893
|
+
const parts = [];
|
|
894
|
+
for (const block of content) {
|
|
895
|
+
if (!isRecord(block)) continue;
|
|
896
|
+
const kind = block["type"];
|
|
897
|
+
if (kind === "text") {
|
|
898
|
+
const text = readString(block["text"]);
|
|
899
|
+
if (text) parts.push({ kind: "text", text });
|
|
900
|
+
} else if (kind === "tool_use") {
|
|
901
|
+
const id = readString(block["id"]);
|
|
902
|
+
const name = readString(block["name"]);
|
|
903
|
+
if (id && name) parts.push({ kind: "tool_call", id, name, input: block["input"] ?? {} });
|
|
904
|
+
} else if (kind === "thinking" || kind === "redacted_thinking") {
|
|
905
|
+
dropped.thinking += 1;
|
|
906
|
+
} else if (kind === "image") {
|
|
907
|
+
dropped.images += 1;
|
|
908
|
+
} else {
|
|
909
|
+
dropped.other += 1;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return parts;
|
|
913
|
+
}
|
|
914
|
+
function contentText(content, dropped) {
|
|
915
|
+
if (typeof content === "string") return content;
|
|
916
|
+
if (!Array.isArray(content)) return "";
|
|
917
|
+
const texts = [];
|
|
918
|
+
for (const block of content) {
|
|
919
|
+
if (!isRecord(block)) continue;
|
|
920
|
+
if (block["type"] === "text") {
|
|
921
|
+
const text = readString(block["text"]);
|
|
922
|
+
if (text) texts.push(text);
|
|
923
|
+
} else if (block["type"] === "image" && dropped) {
|
|
924
|
+
dropped.images += 1;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return texts.join("\n");
|
|
928
|
+
}
|
|
929
|
+
async function inlineOffloaded(output, sidecar, dropped, path) {
|
|
930
|
+
if (!output.startsWith("<persisted-output>")) return output;
|
|
931
|
+
const saved = SAVED_TO.exec(output)?.[1]?.trim();
|
|
932
|
+
const resultsDir = sidecar ? path.join(sidecar, "tool-results") : void 0;
|
|
933
|
+
if (!saved || !resultsDir || path.dirname(saved) !== resultsDir || !saved.endsWith(".txt") || path.basename(saved).includes("..")) {
|
|
934
|
+
dropped.offloaded += 1;
|
|
935
|
+
return output;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const info = await stat2(saved);
|
|
939
|
+
if (!info.isFile() || info.size > OFFLOADED_MAX_BYTES) {
|
|
940
|
+
dropped.offloaded += 1;
|
|
941
|
+
return output;
|
|
942
|
+
}
|
|
943
|
+
return await readFile3(saved, "utf8");
|
|
944
|
+
} catch {
|
|
945
|
+
dropped.offloaded += 1;
|
|
946
|
+
return output;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function countSubagents(dir, path, depth = 0) {
|
|
950
|
+
if (depth > 4) return 0;
|
|
951
|
+
let entries;
|
|
952
|
+
try {
|
|
953
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
954
|
+
} catch {
|
|
955
|
+
return 0;
|
|
956
|
+
}
|
|
957
|
+
let count = 0;
|
|
958
|
+
for (const entry of entries) {
|
|
959
|
+
if (entry.isDirectory()) count += await countSubagents(path.join(dir, entry.name), path, depth + 1);
|
|
960
|
+
else if (entry.isFile() && /^agent-.*\.jsonl$/i.test(entry.name)) count += 1;
|
|
961
|
+
}
|
|
962
|
+
return count;
|
|
963
|
+
}
|
|
964
|
+
var CODEX_APP_ORIGINATOR = "Codex Desktop";
|
|
965
|
+
var INJECTED_PREFIXES = [
|
|
966
|
+
"<environment_context>",
|
|
967
|
+
"<app-context>",
|
|
968
|
+
"<user_instructions>",
|
|
969
|
+
"<permissions instructions>",
|
|
970
|
+
"<turn_aborted>",
|
|
971
|
+
"<user_shell>",
|
|
972
|
+
"# AGENTS.md instructions",
|
|
973
|
+
"<recommended_plugins>",
|
|
974
|
+
"<subagent_notification>",
|
|
975
|
+
"<skill>",
|
|
976
|
+
"<in-app-browser-context",
|
|
977
|
+
// A dump from a voice session, which the history reader in @panoma/core strips as a block.
|
|
978
|
+
"<realtime_delegation>"
|
|
979
|
+
];
|
|
980
|
+
var REQUEST_MARKER = /^##[ \t]+My request(?:[ \t]+for[ \t]+Codex)?:[ \t]*$/m;
|
|
981
|
+
function isCodexInjection(text) {
|
|
982
|
+
const head = text.trimStart();
|
|
983
|
+
return INJECTED_PREFIXES.some((prefix) => head.startsWith(prefix));
|
|
984
|
+
}
|
|
985
|
+
function codexUserText(text) {
|
|
986
|
+
const marker = REQUEST_MARKER.exec(text);
|
|
987
|
+
if (marker !== null) {
|
|
988
|
+
const words = text.slice(marker.index + marker[0].length).trim();
|
|
989
|
+
return words.length > 0 ? words : void 0;
|
|
990
|
+
}
|
|
991
|
+
return isCodexInjection(text) ? void 0 : text;
|
|
992
|
+
}
|
|
993
|
+
async function readCodexConversation(path, options = {}) {
|
|
994
|
+
const store = codexStore(options);
|
|
995
|
+
const text = await readTranscript(path);
|
|
996
|
+
const info = await stat3(path);
|
|
997
|
+
const names = await readSessionIndex(store.index);
|
|
998
|
+
return parseCodexRollout(text, { path, bytes: info.size, mtime: info.mtime.toISOString(), names });
|
|
999
|
+
}
|
|
1000
|
+
function parseCodexRollout(text, input) {
|
|
1001
|
+
const local = nativePath();
|
|
1002
|
+
const { path } = input;
|
|
1003
|
+
const builder = new TurnBuilder();
|
|
1004
|
+
const compactions = [];
|
|
1005
|
+
const dropped = emptyDropped();
|
|
1006
|
+
const head = {
|
|
1007
|
+
agent: "codex-cli",
|
|
1008
|
+
sessionId: codexIdFromName(local.basename(path)) ?? local.basename(path, ".jsonl"),
|
|
1009
|
+
path,
|
|
1010
|
+
cwd: "",
|
|
1011
|
+
updatedAt: input.mtime,
|
|
1012
|
+
bytes: input.bytes,
|
|
1013
|
+
compacted: false,
|
|
1014
|
+
surface: "cli"
|
|
1015
|
+
};
|
|
1016
|
+
let metaSeen = false;
|
|
1017
|
+
let firstUserText;
|
|
1018
|
+
let limit;
|
|
1019
|
+
let lastAt;
|
|
1020
|
+
for (const record2 of parseLines(text)) {
|
|
1021
|
+
if (!isRecord(record2)) continue;
|
|
1022
|
+
const payload = record2["payload"];
|
|
1023
|
+
if (!isRecord(payload)) continue;
|
|
1024
|
+
const type = record2["type"];
|
|
1025
|
+
const at = readString(record2["timestamp"]);
|
|
1026
|
+
if (at) lastAt = at;
|
|
1027
|
+
if (type === "session_meta") {
|
|
1028
|
+
if (metaSeen) continue;
|
|
1029
|
+
metaSeen = true;
|
|
1030
|
+
const id = readString(payload["id"]) ?? readString(payload["session_id"]);
|
|
1031
|
+
if (id) head.sessionId = id;
|
|
1032
|
+
head.cwd = readString(payload["cwd"]) ?? head.cwd;
|
|
1033
|
+
head.startedAt = readString(payload["timestamp"]) ?? at;
|
|
1034
|
+
if (payload["originator"] === CODEX_APP_ORIGINATOR) head.surface = "app";
|
|
1035
|
+
const git = payload["git"];
|
|
1036
|
+
const branch = isRecord(git) ? readString(git["branch"]) : void 0;
|
|
1037
|
+
if (branch) head.gitBranch = branch;
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
if (type === "turn_context") {
|
|
1041
|
+
const cwd = readString(payload["cwd"]);
|
|
1042
|
+
if (cwd) head.cwd = cwd;
|
|
1043
|
+
const model = readString(payload["model"]);
|
|
1044
|
+
if (model && !head.model) head.model = model;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (type === "compacted") {
|
|
1048
|
+
builder.reset();
|
|
1049
|
+
compactions.length = 0;
|
|
1050
|
+
head.compacted = true;
|
|
1051
|
+
const summary = readString(payload["message"]);
|
|
1052
|
+
if (summary) {
|
|
1053
|
+
builder.open("user", [{ kind: "summary", text: summary }], at);
|
|
1054
|
+
const compaction = { text: summary };
|
|
1055
|
+
if (at) compaction.at = at;
|
|
1056
|
+
compactions.push(compaction);
|
|
1057
|
+
}
|
|
1058
|
+
const history = payload["replacement_history"];
|
|
1059
|
+
if (Array.isArray(history)) {
|
|
1060
|
+
for (const item of history) {
|
|
1061
|
+
if (isRecord(item)) replayItem(item, builder, dropped, at, (t) => {
|
|
1062
|
+
firstUserText ??= t;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if (type === "event_msg") {
|
|
1069
|
+
if (payload["type"] === "token_count") limit = limitOf2(payload, at);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (type !== "response_item") continue;
|
|
1073
|
+
replayItem(payload, builder, dropped, at, (t) => {
|
|
1074
|
+
firstUserText ??= t;
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
if (lastAt) head.updatedAt = lastAt;
|
|
1078
|
+
head.title = input.names?.get(head.sessionId) ?? (firstUserText ? titleFromText(firstUserText) : void 0);
|
|
1079
|
+
if (limit) head.limit = limit;
|
|
1080
|
+
return finishConversation(head, builder.turns, compactions, dropped);
|
|
1081
|
+
}
|
|
1082
|
+
function replayItem(item, builder, dropped, at, onUserText) {
|
|
1083
|
+
const kind = item["type"];
|
|
1084
|
+
if (kind === "message") {
|
|
1085
|
+
const role = item["role"];
|
|
1086
|
+
if (role !== "user" && role !== "assistant") {
|
|
1087
|
+
dropped.other += 1;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const texts = contentTexts(item["content"], dropped);
|
|
1091
|
+
for (const raw of texts) {
|
|
1092
|
+
const text = role === "user" ? codexUserText(raw) : raw;
|
|
1093
|
+
if (text === void 0) {
|
|
1094
|
+
dropped.other += 1;
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (role === "user") onUserText(text);
|
|
1098
|
+
builder.push(role, { kind: "text", text }, at);
|
|
1099
|
+
}
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
|
|
1103
|
+
const id = readString(item["call_id"]) ?? readString(item["id"]);
|
|
1104
|
+
if (!id) return;
|
|
1105
|
+
const name = readString(item["name"]) ?? (kind === "local_shell_call" ? "shell" : "tool");
|
|
1106
|
+
const raw = kind === "function_call" ? parseArguments(item["arguments"]) : kind === "custom_tool_call" ? item["input"] : item["action"];
|
|
1107
|
+
builder.push("assistant", { kind: "tool_call", id, name, input: toolInput(raw) }, at);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (kind === "function_call_output" || kind === "custom_tool_call_output") {
|
|
1111
|
+
const callId = readString(item["call_id"]);
|
|
1112
|
+
if (!callId) return;
|
|
1113
|
+
const raw = item["output"];
|
|
1114
|
+
const output = typeof raw === "string" ? raw : contentTexts(raw, dropped).join("\n");
|
|
1115
|
+
builder.push("user", { kind: "tool_result", callId, output }, at);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (kind === "reasoning") {
|
|
1119
|
+
dropped.thinking += 1;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
dropped.other += 1;
|
|
1123
|
+
}
|
|
1124
|
+
function parseArguments(value) {
|
|
1125
|
+
if (typeof value !== "string") return value ?? {};
|
|
1126
|
+
try {
|
|
1127
|
+
return JSON.parse(value);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return value;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function toolInput(value) {
|
|
1133
|
+
if (isRecord(value)) return value;
|
|
1134
|
+
return { input: value ?? "" };
|
|
1135
|
+
}
|
|
1136
|
+
function contentTexts(content, dropped) {
|
|
1137
|
+
if (typeof content === "string") return content.length > 0 ? [content] : [];
|
|
1138
|
+
if (!Array.isArray(content)) return [];
|
|
1139
|
+
const texts = [];
|
|
1140
|
+
for (const block of content) {
|
|
1141
|
+
if (!isRecord(block)) continue;
|
|
1142
|
+
const type = block["type"];
|
|
1143
|
+
if (type === "input_text" || type === "output_text" || type === "text") {
|
|
1144
|
+
const text = readString(block["text"]);
|
|
1145
|
+
if (text) texts.push(text);
|
|
1146
|
+
} else if (type === "input_image" || type === "image") {
|
|
1147
|
+
dropped.images += 1;
|
|
1148
|
+
} else if (type === "encrypted_content") {
|
|
1149
|
+
dropped.thinking += 1;
|
|
1150
|
+
} else {
|
|
1151
|
+
dropped.other += 1;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return texts;
|
|
1155
|
+
}
|
|
1156
|
+
function limitOf2(payload, at) {
|
|
1157
|
+
const limits = payload["rate_limits"];
|
|
1158
|
+
if (!isRecord(limits)) return void 0;
|
|
1159
|
+
const reached = readString(limits["rate_limit_reached_type"]);
|
|
1160
|
+
const windows = [["primary", limits["primary"]], ["secondary", limits["secondary"]]];
|
|
1161
|
+
let full;
|
|
1162
|
+
for (const [name, window] of windows) {
|
|
1163
|
+
if (!isRecord(window)) continue;
|
|
1164
|
+
const used = readNumber(window["used_percent"]);
|
|
1165
|
+
if (used !== void 0 && used >= 100) {
|
|
1166
|
+
full = { name };
|
|
1167
|
+
const resetsAt2 = isoFromEpochSeconds(window["resets_at"]);
|
|
1168
|
+
if (resetsAt2) full.resetsAt = resetsAt2;
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (!reached && !full) return void 0;
|
|
1173
|
+
const hit = { at: at ?? "", kind: reached ?? full.name };
|
|
1174
|
+
const resetsAt = full?.resetsAt ?? soonestReset(windows);
|
|
1175
|
+
if (resetsAt) hit.resetsAt = resetsAt;
|
|
1176
|
+
return hit;
|
|
1177
|
+
}
|
|
1178
|
+
function soonestReset(windows) {
|
|
1179
|
+
let best;
|
|
1180
|
+
for (const [, window] of windows) {
|
|
1181
|
+
if (!isRecord(window)) continue;
|
|
1182
|
+
const iso = isoFromEpochSeconds(window["resets_at"]);
|
|
1183
|
+
if (iso && (!best || iso < best)) best = iso;
|
|
1184
|
+
}
|
|
1185
|
+
return best;
|
|
1186
|
+
}
|
|
1187
|
+
async function readSessionIndex(indexPath) {
|
|
1188
|
+
const names = /* @__PURE__ */ new Map();
|
|
1189
|
+
let text;
|
|
1190
|
+
try {
|
|
1191
|
+
text = await readFile4(indexPath, "utf8");
|
|
1192
|
+
} catch {
|
|
1193
|
+
return names;
|
|
1194
|
+
}
|
|
1195
|
+
for (const record2 of parseLines(text)) {
|
|
1196
|
+
if (!isRecord(record2)) continue;
|
|
1197
|
+
const id = readString(record2["id"]);
|
|
1198
|
+
const name = readString(record2["thread_name"]);
|
|
1199
|
+
if (id && name) names.set(id, name);
|
|
1200
|
+
}
|
|
1201
|
+
return names;
|
|
1202
|
+
}
|
|
1203
|
+
async function readGeminiConversation(path, options = {}, deps = {}) {
|
|
1204
|
+
const store = geminiStore(options);
|
|
1205
|
+
const text = await readTranscript(path);
|
|
1206
|
+
const local = nativePath();
|
|
1207
|
+
const info = await stat4(path);
|
|
1208
|
+
const projectId = local.basename(local.dirname(local.dirname(path)));
|
|
1209
|
+
const cwd = geminiProjectFolders(store, deps.cwds ?? []).get(projectId);
|
|
1210
|
+
const input = {
|
|
1211
|
+
path,
|
|
1212
|
+
bytes: info.size,
|
|
1213
|
+
mtime: info.mtime.toISOString(),
|
|
1214
|
+
legacy: path.toLowerCase().endsWith(".json")
|
|
1215
|
+
};
|
|
1216
|
+
if (cwd) input.cwd = cwd;
|
|
1217
|
+
return parseGeminiChat(text, input);
|
|
1218
|
+
}
|
|
1219
|
+
function parseGeminiChat(text, input) {
|
|
1220
|
+
const local = nativePath();
|
|
1221
|
+
const { path } = input;
|
|
1222
|
+
const records = input.legacy ? legacyRecords(text) : parseLines(text);
|
|
1223
|
+
const builder = new TurnBuilder();
|
|
1224
|
+
const dropped = emptyDropped();
|
|
1225
|
+
const head = {
|
|
1226
|
+
agent: "gemini-cli",
|
|
1227
|
+
sessionId: "",
|
|
1228
|
+
path,
|
|
1229
|
+
cwd: input.cwd ?? "",
|
|
1230
|
+
updatedAt: input.mtime,
|
|
1231
|
+
bytes: input.bytes,
|
|
1232
|
+
compacted: false
|
|
1233
|
+
};
|
|
1234
|
+
let summary;
|
|
1235
|
+
let firstUserText;
|
|
1236
|
+
const messages = /* @__PURE__ */ new Map();
|
|
1237
|
+
for (const record2 of records) {
|
|
1238
|
+
if (!isRecord(record2)) continue;
|
|
1239
|
+
const set = record2["$set"];
|
|
1240
|
+
if (isRecord(set)) {
|
|
1241
|
+
const title2 = readString(set["summary"]);
|
|
1242
|
+
if (title2) summary = title2;
|
|
1243
|
+
const replaced = set["messages"];
|
|
1244
|
+
if (Array.isArray(replaced)) {
|
|
1245
|
+
messages.clear();
|
|
1246
|
+
replaced.forEach((m, i) => {
|
|
1247
|
+
if (isRecord(m)) messages.set(readString(m["id"]) ?? `#${i}`, m);
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
const rewind = readString(record2["$rewindTo"]);
|
|
1253
|
+
if (rewind) {
|
|
1254
|
+
let cut3 = false;
|
|
1255
|
+
for (const id2 of [...messages.keys()]) {
|
|
1256
|
+
if (id2 === rewind) cut3 = true;
|
|
1257
|
+
if (cut3) messages.delete(id2);
|
|
1258
|
+
}
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const messageId = record2["id"];
|
|
1262
|
+
if (typeof messageId === "string") {
|
|
1263
|
+
messages.set(messageId, record2);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const id = readString(record2["sessionId"]);
|
|
1267
|
+
if (id) head.sessionId = id;
|
|
1268
|
+
const started = readString(record2["startTime"]);
|
|
1269
|
+
if (started) head.startedAt = started;
|
|
1270
|
+
const updated = readString(record2["lastUpdated"]);
|
|
1271
|
+
if (updated) head.updatedAt = updated;
|
|
1272
|
+
const title = readString(record2["summary"]);
|
|
1273
|
+
if (title) summary = title;
|
|
1274
|
+
}
|
|
1275
|
+
if (!head.sessionId) head.sessionId = local.basename(path).replace(/\.jsonl?$/i, "");
|
|
1276
|
+
for (const message of messages.values()) {
|
|
1277
|
+
const at = readString(message["timestamp"]);
|
|
1278
|
+
if (at) head.updatedAt = at;
|
|
1279
|
+
const type = message["type"];
|
|
1280
|
+
if (type === "user") {
|
|
1281
|
+
const texts = contentTexts2(message["content"], dropped);
|
|
1282
|
+
for (const t of texts) {
|
|
1283
|
+
firstUserText ??= t;
|
|
1284
|
+
builder.push("user", { kind: "text", text: t }, at);
|
|
1285
|
+
}
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (type !== "gemini") {
|
|
1289
|
+
dropped.other += 1;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
const model = readString(message["model"]);
|
|
1293
|
+
if (model && !head.model) head.model = model;
|
|
1294
|
+
if (Array.isArray(message["thoughts"]) && message["thoughts"].length > 0) dropped.thinking += 1;
|
|
1295
|
+
for (const t of contentTexts2(message["content"], dropped)) {
|
|
1296
|
+
builder.push("assistant", { kind: "text", text: t }, at);
|
|
1297
|
+
}
|
|
1298
|
+
for (const call of Array.isArray(message["toolCalls"]) ? message["toolCalls"] : []) {
|
|
1299
|
+
if (!isRecord(call)) continue;
|
|
1300
|
+
const id = readString(call["id"]);
|
|
1301
|
+
const name = readString(call["name"]);
|
|
1302
|
+
if (!id || !name) continue;
|
|
1303
|
+
builder.push("assistant", { kind: "tool_call", id, name, input: call["args"] ?? {} }, at);
|
|
1304
|
+
const output = responseText(call["result"]);
|
|
1305
|
+
if (output !== void 0) {
|
|
1306
|
+
const result = { kind: "tool_result", callId: id, output };
|
|
1307
|
+
if (call["status"] === "error") result.isError = true;
|
|
1308
|
+
builder.push("user", result, at);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
head.title = summary ?? (firstUserText ? titleFromText(firstUserText) : void 0);
|
|
1313
|
+
return finishConversation(head, builder.turns, [], dropped);
|
|
1314
|
+
}
|
|
1315
|
+
function legacyRecords(text) {
|
|
1316
|
+
let parsed;
|
|
1317
|
+
try {
|
|
1318
|
+
parsed = JSON.parse(text);
|
|
1319
|
+
} catch {
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1322
|
+
if (!isRecord(parsed)) return [];
|
|
1323
|
+
const { messages, ...meta } = parsed;
|
|
1324
|
+
return [meta, ...Array.isArray(messages) ? messages : []];
|
|
1325
|
+
}
|
|
1326
|
+
function contentTexts2(content, dropped) {
|
|
1327
|
+
if (typeof content === "string") return content.length > 0 ? [content] : [];
|
|
1328
|
+
if (!Array.isArray(content)) return [];
|
|
1329
|
+
const texts = [];
|
|
1330
|
+
for (const part of content) {
|
|
1331
|
+
if (!isRecord(part)) continue;
|
|
1332
|
+
const text = readString(part["text"]);
|
|
1333
|
+
if (text) texts.push(text);
|
|
1334
|
+
else if (part["inlineData"] !== void 0 || part["fileData"] !== void 0) dropped.images += 1;
|
|
1335
|
+
else if (part["functionCall"] === void 0 && part["functionResponse"] === void 0) dropped.other += 1;
|
|
1336
|
+
}
|
|
1337
|
+
return texts;
|
|
1338
|
+
}
|
|
1339
|
+
function responseText(result) {
|
|
1340
|
+
if (result === void 0 || result === null) return void 0;
|
|
1341
|
+
if (typeof result === "string") return result;
|
|
1342
|
+
if (!Array.isArray(result)) return JSON.stringify(result);
|
|
1343
|
+
const texts = [];
|
|
1344
|
+
for (const part of result) {
|
|
1345
|
+
if (!isRecord(part)) continue;
|
|
1346
|
+
const response = isRecord(part["functionResponse"]) ? part["functionResponse"]["response"] : void 0;
|
|
1347
|
+
if (isRecord(response)) {
|
|
1348
|
+
const output = response["output"] ?? response["error"];
|
|
1349
|
+
texts.push(typeof output === "string" ? output : JSON.stringify(response));
|
|
1350
|
+
} else if (typeof part["text"] === "string") {
|
|
1351
|
+
texts.push(part["text"]);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return texts.join("\n");
|
|
1355
|
+
}
|
|
1356
|
+
async function readOpencodeConversation(source, options = {}, deps = {}) {
|
|
1357
|
+
resolveStoreOptions(options);
|
|
1358
|
+
const local = nativePath();
|
|
1359
|
+
const name = local.basename(source.path);
|
|
1360
|
+
if (opencodeEnvelopeId(name)) return readEnvelope(source.path, source.sessionId);
|
|
1361
|
+
const isDb = name.toLowerCase().endsWith(".db");
|
|
1362
|
+
const root = isDb ? local.dirname(source.path) : source.path;
|
|
1363
|
+
if (isDb) {
|
|
1364
|
+
const fromDb = await readFromDatabase(source.path, source.sessionId, deps);
|
|
1365
|
+
if (fromDb) return fromDb;
|
|
1366
|
+
}
|
|
1367
|
+
return readFromStorage(local.join(root, "storage"), source.sessionId, source.path);
|
|
1368
|
+
}
|
|
1369
|
+
async function openSqlite(deps = {}) {
|
|
1370
|
+
if (deps.sqlite) return deps.sqlite;
|
|
1371
|
+
try {
|
|
1372
|
+
const specifier = ["node", "sqlite"].join(":");
|
|
1373
|
+
const mod = await import(
|
|
1374
|
+
/* webpackIgnore: true */
|
|
1375
|
+
/* @vite-ignore */
|
|
1376
|
+
specifier
|
|
1377
|
+
);
|
|
1378
|
+
return {
|
|
1379
|
+
open(path) {
|
|
1380
|
+
const db = new mod.DatabaseSync(path, { readOnly: true });
|
|
1381
|
+
return {
|
|
1382
|
+
all: (sql, ...params) => db.prepare(sql).all(...params),
|
|
1383
|
+
close: () => db.close()
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
} catch {
|
|
1388
|
+
return void 0;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async function readFromDatabase(dbPath, sessionId, deps) {
|
|
1392
|
+
const sqlite = await openSqlite(deps);
|
|
1393
|
+
if (!sqlite) return void 0;
|
|
1394
|
+
let db;
|
|
1395
|
+
try {
|
|
1396
|
+
db = sqlite.open(dbPath);
|
|
1397
|
+
} catch {
|
|
1398
|
+
return void 0;
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
const sessions = db.all(
|
|
1402
|
+
"SELECT id, directory, title, time_created, time_updated FROM session WHERE id = ?",
|
|
1403
|
+
sessionId
|
|
1404
|
+
);
|
|
1405
|
+
const row = sessions[0];
|
|
1406
|
+
if (!row) throw new HandoffFault("conversation-not-found", sessionId);
|
|
1407
|
+
const session = {
|
|
1408
|
+
id: sessionId,
|
|
1409
|
+
directory: readString(row["directory"]) ?? "",
|
|
1410
|
+
created: readNumber(row["time_created"]),
|
|
1411
|
+
updated: readNumber(row["time_updated"])
|
|
1412
|
+
};
|
|
1413
|
+
const title = readString(row["title"]);
|
|
1414
|
+
if (title) session.title = title;
|
|
1415
|
+
const messages = db.all("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created, id", sessionId).map(rowOf).filter((r) => r !== void 0);
|
|
1416
|
+
const parts = /* @__PURE__ */ new Map();
|
|
1417
|
+
for (const raw of db.all(
|
|
1418
|
+
"SELECT id, message_id, time_created, data FROM part WHERE session_id = ? ORDER BY time_created, id",
|
|
1419
|
+
sessionId
|
|
1420
|
+
)) {
|
|
1421
|
+
const part = rowOf(raw);
|
|
1422
|
+
const messageId = readString(raw["message_id"]);
|
|
1423
|
+
if (!part || !messageId) continue;
|
|
1424
|
+
const list2 = parts.get(messageId) ?? [];
|
|
1425
|
+
list2.push(part);
|
|
1426
|
+
parts.set(messageId, list2);
|
|
1427
|
+
}
|
|
1428
|
+
const bytes = readNumber(
|
|
1429
|
+
db.all("SELECT COALESCE(SUM(LENGTH(data)), 0) AS bytes FROM message WHERE session_id = ?", sessionId)[0]?.["bytes"]
|
|
1430
|
+
) ?? 0;
|
|
1431
|
+
const partBytes = readNumber(
|
|
1432
|
+
db.all("SELECT COALESCE(SUM(LENGTH(data)), 0) AS bytes FROM part WHERE session_id = ?", sessionId)[0]?.["bytes"]
|
|
1433
|
+
) ?? 0;
|
|
1434
|
+
return assemble(session, messages, parts, dbPath, bytes + partBytes);
|
|
1435
|
+
} finally {
|
|
1436
|
+
db.close();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function rowOf(raw) {
|
|
1440
|
+
const id = readString(raw["id"]);
|
|
1441
|
+
const created = readNumber(raw["time_created"]) ?? 0;
|
|
1442
|
+
const data = raw["data"];
|
|
1443
|
+
let parsed = data;
|
|
1444
|
+
if (typeof data === "string") {
|
|
1445
|
+
try {
|
|
1446
|
+
parsed = JSON.parse(data);
|
|
1447
|
+
} catch {
|
|
1448
|
+
return void 0;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (!id || !isRecord(parsed)) return void 0;
|
|
1452
|
+
return { id, created, data: parsed };
|
|
1453
|
+
}
|
|
1454
|
+
async function readFromStorage(storage, sessionId, shownPath) {
|
|
1455
|
+
const local = nativePath();
|
|
1456
|
+
const sessionFile = await findSessionFile(local.join(storage, "session"), sessionId);
|
|
1457
|
+
if (!sessionFile) throw new HandoffFault("conversation-not-found", sessionId);
|
|
1458
|
+
const info = await readJson(sessionFile);
|
|
1459
|
+
if (!isRecord(info)) throw new HandoffFault("unreadable-transcript", sessionFile);
|
|
1460
|
+
const time = isRecord(info["time"]) ? info["time"] : {};
|
|
1461
|
+
const session = {
|
|
1462
|
+
id: sessionId,
|
|
1463
|
+
directory: readString(info["directory"]) ?? "",
|
|
1464
|
+
created: readNumber(time["created"]),
|
|
1465
|
+
updated: readNumber(time["updated"])
|
|
1466
|
+
};
|
|
1467
|
+
const title = readString(info["title"]);
|
|
1468
|
+
if (title) session.title = title;
|
|
1469
|
+
let bytes = 0;
|
|
1470
|
+
const messages = [];
|
|
1471
|
+
const parts = /* @__PURE__ */ new Map();
|
|
1472
|
+
const messageDir = local.join(storage, "message", sessionId);
|
|
1473
|
+
for (const file of await listJson(messageDir)) {
|
|
1474
|
+
const data = await readJson(local.join(messageDir, file));
|
|
1475
|
+
bytes += (await stat5(local.join(messageDir, file)).catch(() => ({ size: 0 }))).size;
|
|
1476
|
+
if (!isRecord(data)) continue;
|
|
1477
|
+
const id = readString(data["id"]) ?? local.basename(file, ".json");
|
|
1478
|
+
const created = isRecord(data["time"]) ? readNumber(data["time"]["created"]) ?? 0 : 0;
|
|
1479
|
+
messages.push({ id, created, data });
|
|
1480
|
+
const partDir = local.join(storage, "part", id);
|
|
1481
|
+
const list2 = [];
|
|
1482
|
+
for (const partFile of await listJson(partDir)) {
|
|
1483
|
+
const part = await readJson(local.join(partDir, partFile));
|
|
1484
|
+
bytes += (await stat5(local.join(partDir, partFile)).catch(() => ({ size: 0 }))).size;
|
|
1485
|
+
if (!isRecord(part)) continue;
|
|
1486
|
+
const partId = readString(part["id"]) ?? local.basename(partFile, ".json");
|
|
1487
|
+
const time2 = isRecord(part["time"]) ? readNumber(part["time"]["start"]) ?? 0 : 0;
|
|
1488
|
+
list2.push({ id: partId, created: time2, data: part });
|
|
1489
|
+
}
|
|
1490
|
+
parts.set(id, sortRows(list2));
|
|
1491
|
+
}
|
|
1492
|
+
return assemble(session, sortRows(messages), parts, shownPath, bytes);
|
|
1493
|
+
}
|
|
1494
|
+
function sortRows(rows) {
|
|
1495
|
+
return rows.sort((a, b) => a.created - b.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1496
|
+
}
|
|
1497
|
+
async function findSessionFile(sessionRoot, sessionId) {
|
|
1498
|
+
const local = nativePath();
|
|
1499
|
+
let projects;
|
|
1500
|
+
try {
|
|
1501
|
+
projects = await readdir2(sessionRoot);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return void 0;
|
|
1504
|
+
}
|
|
1505
|
+
for (const project of projects) {
|
|
1506
|
+
const candidate = local.join(sessionRoot, project, `${sessionId}.json`);
|
|
1507
|
+
try {
|
|
1508
|
+
if ((await stat5(candidate)).isFile()) return candidate;
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return void 0;
|
|
1513
|
+
}
|
|
1514
|
+
async function listJson(dir) {
|
|
1515
|
+
try {
|
|
1516
|
+
return (await readdir2(dir)).filter((name) => name.endsWith(".json")).sort();
|
|
1517
|
+
} catch {
|
|
1518
|
+
return [];
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function readJson(path) {
|
|
1522
|
+
try {
|
|
1523
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
throw asHandoffFault(error, "unreadable-transcript");
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async function readEnvelope(path, sessionId) {
|
|
1529
|
+
const envelope = await readJson(path);
|
|
1530
|
+
if (!isRecord(envelope) || !isRecord(envelope["info"]) || !Array.isArray(envelope["messages"])) {
|
|
1531
|
+
throw new HandoffFault("unreadable-transcript", path);
|
|
1532
|
+
}
|
|
1533
|
+
const info = envelope["info"];
|
|
1534
|
+
const time = isRecord(info["time"]) ? info["time"] : {};
|
|
1535
|
+
const session = {
|
|
1536
|
+
id: readString(info["id"]) ?? sessionId,
|
|
1537
|
+
directory: readString(info["directory"]) ?? "",
|
|
1538
|
+
created: readNumber(time["created"]),
|
|
1539
|
+
updated: readNumber(time["updated"])
|
|
1540
|
+
};
|
|
1541
|
+
const title = readString(info["title"]);
|
|
1542
|
+
if (title) session.title = title;
|
|
1543
|
+
const messages = [];
|
|
1544
|
+
const parts = /* @__PURE__ */ new Map();
|
|
1545
|
+
for (const entry of envelope["messages"]) {
|
|
1546
|
+
if (!isRecord(entry) || !isRecord(entry["info"])) continue;
|
|
1547
|
+
const data = entry["info"];
|
|
1548
|
+
const id = readString(data["id"]);
|
|
1549
|
+
if (!id) continue;
|
|
1550
|
+
const created = isRecord(data["time"]) ? readNumber(data["time"]["created"]) ?? 0 : 0;
|
|
1551
|
+
messages.push({ id, created, data });
|
|
1552
|
+
const list2 = [];
|
|
1553
|
+
for (const part of Array.isArray(entry["parts"]) ? entry["parts"] : []) {
|
|
1554
|
+
if (!isRecord(part)) continue;
|
|
1555
|
+
list2.push({ id: readString(part["id"]) ?? "", created: 0, data: part });
|
|
1556
|
+
}
|
|
1557
|
+
parts.set(id, list2);
|
|
1558
|
+
}
|
|
1559
|
+
const bytes = (await stat5(path)).size;
|
|
1560
|
+
return assemble(session, messages, parts, path, bytes);
|
|
1561
|
+
}
|
|
1562
|
+
function assemble(session, messages, partsByMessage, path, bytes) {
|
|
1563
|
+
const builder = new TurnBuilder();
|
|
1564
|
+
const compactions = [];
|
|
1565
|
+
const dropped = emptyDropped();
|
|
1566
|
+
const head = {
|
|
1567
|
+
agent: "opencode",
|
|
1568
|
+
sessionId: session.id,
|
|
1569
|
+
path,
|
|
1570
|
+
cwd: session.directory,
|
|
1571
|
+
updatedAt: isoFromEpochMs(session.updated) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1572
|
+
bytes,
|
|
1573
|
+
compacted: false
|
|
1574
|
+
};
|
|
1575
|
+
const startedAt = isoFromEpochMs(session.created);
|
|
1576
|
+
if (startedAt) head.startedAt = startedAt;
|
|
1577
|
+
if (session.title) head.title = session.title;
|
|
1578
|
+
let limit;
|
|
1579
|
+
let pendingSummary = false;
|
|
1580
|
+
for (const message of messages) {
|
|
1581
|
+
const data = message.data;
|
|
1582
|
+
const role = data["role"];
|
|
1583
|
+
const at = isoFromEpochMs(isRecord(data["time"]) ? data["time"]["created"] : message.created);
|
|
1584
|
+
const parts = partsByMessage.get(message.id) ?? [];
|
|
1585
|
+
if (role !== "user" && role !== "assistant") {
|
|
1586
|
+
dropped.other += 1;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (role === "user") {
|
|
1590
|
+
if (parts.some((p) => p.data["type"] === "compaction")) {
|
|
1591
|
+
builder.reset();
|
|
1592
|
+
compactions.length = 0;
|
|
1593
|
+
head.compacted = true;
|
|
1594
|
+
pendingSummary = true;
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
for (const part of parts) userPart(part.data, builder, dropped, at);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const provider = readString(data["providerID"]);
|
|
1601
|
+
const model = readString(data["modelID"]);
|
|
1602
|
+
if (model) head.model = provider ? `${provider}/${model}` : model;
|
|
1603
|
+
const error = data["error"];
|
|
1604
|
+
if (isRecord(error)) {
|
|
1605
|
+
limit = limitOf3(error, at) ?? limit;
|
|
1606
|
+
if (parts.every((p) => p.data["type"] !== "text")) {
|
|
1607
|
+
dropped.other += 1;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
} else {
|
|
1611
|
+
limit = void 0;
|
|
1612
|
+
}
|
|
1613
|
+
if (pendingSummary && data["summary"] === true) {
|
|
1614
|
+
pendingSummary = false;
|
|
1615
|
+
const text = parts.filter((p) => p.data["type"] === "text").map((p) => readString(p.data["text"]) ?? "").filter((t) => t.length > 0).join("\n");
|
|
1616
|
+
builder.open("user", [{ kind: "summary", text }], at);
|
|
1617
|
+
const compaction = { text };
|
|
1618
|
+
if (at) compaction.at = at;
|
|
1619
|
+
compactions.push(compaction);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
for (const part of parts) assistantPart(part.data, builder, dropped, at);
|
|
1623
|
+
}
|
|
1624
|
+
if (limit) head.limit = limit;
|
|
1625
|
+
return finishConversation(head, builder.turns, compactions, dropped);
|
|
1626
|
+
}
|
|
1627
|
+
function userPart(data, builder, dropped, at) {
|
|
1628
|
+
const type = data["type"];
|
|
1629
|
+
if (type === "text") {
|
|
1630
|
+
const text = readString(data["text"]);
|
|
1631
|
+
if (text) builder.push("user", { kind: "text", text }, at);
|
|
1632
|
+
} else if (type === "file") {
|
|
1633
|
+
const mime = readString(data["mime"]) ?? "";
|
|
1634
|
+
if (mime.startsWith("image/")) dropped.images += 1;
|
|
1635
|
+
else dropped.other += 1;
|
|
1636
|
+
} else {
|
|
1637
|
+
dropped.other += 1;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
function assistantPart(data, builder, dropped, at) {
|
|
1641
|
+
const type = data["type"];
|
|
1642
|
+
if (type === "text") {
|
|
1643
|
+
const text = readString(data["text"]);
|
|
1644
|
+
if (text) builder.push("assistant", { kind: "text", text }, at);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (type === "tool") {
|
|
1648
|
+
const id = readString(data["callID"]) ?? readString(data["id"]);
|
|
1649
|
+
const name = readString(data["tool"]) ?? "tool";
|
|
1650
|
+
if (!id) return;
|
|
1651
|
+
const state = isRecord(data["state"]) ? data["state"] : {};
|
|
1652
|
+
builder.push("assistant", { kind: "tool_call", id, name, input: state["input"] ?? {} }, at);
|
|
1653
|
+
const status = readString(state["status"]);
|
|
1654
|
+
if (status === "completed" || status === "error") {
|
|
1655
|
+
const output = readString(state["output"]) ?? readString(state["error"]) ?? "";
|
|
1656
|
+
const result = { kind: "tool_result", callId: id, output };
|
|
1657
|
+
builder.push("user", status === "error" ? { ...result, isError: true } : result, at);
|
|
1658
|
+
}
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (type === "reasoning") {
|
|
1662
|
+
dropped.thinking += 1;
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
if (type === "subtask") {
|
|
1666
|
+
dropped.subagents += 1;
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
if (type === "file") {
|
|
1670
|
+
const mime = readString(data["mime"]) ?? "";
|
|
1671
|
+
if (mime.startsWith("image/")) dropped.images += 1;
|
|
1672
|
+
else dropped.other += 1;
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function limitOf3(error, at) {
|
|
1677
|
+
const data = isRecord(error["data"]) ? error["data"] : {};
|
|
1678
|
+
const status = readNumber(data["statusCode"]);
|
|
1679
|
+
if (status !== 429 && status !== 402) return void 0;
|
|
1680
|
+
return { at: at ?? "", kind: status === 429 ? "rate-limit" : "credits" };
|
|
1681
|
+
}
|
|
1682
|
+
async function readConversation(source, options = {}, deps = {}) {
|
|
1683
|
+
switch (source.agent) {
|
|
1684
|
+
case "claude-cli":
|
|
1685
|
+
return readClaudeConversation(source.path, options);
|
|
1686
|
+
case "codex-cli":
|
|
1687
|
+
return readCodexConversation(source.path, options);
|
|
1688
|
+
case "opencode": {
|
|
1689
|
+
if (!source.sessionId) throw new HandoffFault("invalid-id", "an OpenCode session id is needed");
|
|
1690
|
+
const opencodeDeps = deps.sqlite ? { sqlite: deps.sqlite } : {};
|
|
1691
|
+
return readOpencodeConversation({ path: source.path, sessionId: source.sessionId }, options, opencodeDeps);
|
|
1692
|
+
}
|
|
1693
|
+
case "gemini-cli":
|
|
1694
|
+
return readGeminiConversation(source.path, options, deps.cwds ? { cwds: deps.cwds } : {});
|
|
1695
|
+
default:
|
|
1696
|
+
throw new HandoffFault("unsupported-target", source.agent);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
var TITLE_CHARS = 80;
|
|
1700
|
+
var GOAL_CHARS = 600;
|
|
1701
|
+
var LINE_CHARS = 200;
|
|
1702
|
+
var EXCHANGE_CHARS = 600;
|
|
1703
|
+
var MAX_DECISIONS = 8;
|
|
1704
|
+
var MAX_FILES = 40;
|
|
1705
|
+
var MAX_COMMANDS = 30;
|
|
1706
|
+
var MAX_OPEN_ITEMS = 8;
|
|
1707
|
+
var DECISION_STARTS = /^(?:[-*•]\s*)?(?:\*\*)?(?:decision|decided|we will|we'll|i'll|i will|let's go with|going with|chose|choosing|settled on|the plan is)\b/i;
|
|
1708
|
+
var OPEN_ITEM_LINE = /^(?:[-*•]\s*\[ \]\s*|(?:[-*•]|\d+[.)])\s*(?:todo|next|then|remaining|pending|still|left to|to do)\b|todo\b|next(?: step)?s?\b|pending\b|remaining\b|still to\b|left to\b)/i;
|
|
1709
|
+
var OPEN_ITEM_HEADING = /^#{0,6}\s*(?:\*\*)?(?:next steps?|open items?|remaining|pending|todos?|what's left|left to do)(?:\*\*)?\s*:?\s*$/i;
|
|
1710
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit", "apply_patch", "edit", "write", "multiedit"]);
|
|
1711
|
+
var PATH_KEYS = ["file_path", "path", "notebook_path", "filePath"];
|
|
1712
|
+
var PATCH_FILE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
|
1713
|
+
var SHELL_TOOLS = /* @__PURE__ */ new Set(["Bash", "bash", "exec", "shell", "exec_command", "run_shell_command", "shell_command"]);
|
|
1714
|
+
var COMMAND_MARKER = /^\s*<command-(?:name|message)>[\s\S]*$/;
|
|
1715
|
+
function digestConversation(conversation) {
|
|
1716
|
+
const turns = conversation.turns;
|
|
1717
|
+
const firstUser = firstText(turns, "user", (text) => !COMMAND_MARKER.test(text)) ?? firstText(turns, "user");
|
|
1718
|
+
const title = conversation.title ?? (firstUser ? cutLine(firstLine(firstUser), TITLE_CHARS) : "Untitled conversation");
|
|
1719
|
+
const goal = firstUser ? cut2(firstUser, GOAL_CHARS) : "";
|
|
1720
|
+
const summary = conversation.compactions[conversation.compactions.length - 1]?.text;
|
|
1721
|
+
const decisions = [];
|
|
1722
|
+
const files = /* @__PURE__ */ new Set();
|
|
1723
|
+
const commands = [];
|
|
1724
|
+
let toolCalls = 0;
|
|
1725
|
+
for (const turn of turns) {
|
|
1726
|
+
for (const part of turn.parts) {
|
|
1727
|
+
if (part.kind === "text" && turn.role === "assistant") {
|
|
1728
|
+
for (const line2 of part.text.split(/\r?\n/)) {
|
|
1729
|
+
const clean = line2.trim();
|
|
1730
|
+
if (decisions.length < MAX_DECISIONS && DECISION_STARTS.test(clean)) decisions.push(cutLine(clean, LINE_CHARS));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (part.kind !== "tool_call") continue;
|
|
1734
|
+
toolCalls += 1;
|
|
1735
|
+
for (const file of filesOf(part)) if (files.size < MAX_FILES) files.add(file);
|
|
1736
|
+
const command = commandOf(part);
|
|
1737
|
+
if (command && commands.length < MAX_COMMANDS) commands.push(cutLine(command, LINE_CHARS));
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const lastUser = lastText(turns, "user");
|
|
1741
|
+
const lastAssistant = lastText(turns, "assistant");
|
|
1742
|
+
const lastExchange = {};
|
|
1743
|
+
if (lastUser) lastExchange.user = cut2(lastUser, EXCHANGE_CHARS);
|
|
1744
|
+
if (lastAssistant) lastExchange.assistant = cut2(lastAssistant, EXCHANGE_CHARS);
|
|
1745
|
+
const digest = {
|
|
1746
|
+
by: "panoma",
|
|
1747
|
+
title,
|
|
1748
|
+
goal,
|
|
1749
|
+
decisions,
|
|
1750
|
+
filesTouched: [...files],
|
|
1751
|
+
commandsRun: commands,
|
|
1752
|
+
openItems: lastAssistant ? openItemsOf(lastAssistant) : [],
|
|
1753
|
+
lastExchange,
|
|
1754
|
+
stats: { turns: turns.length, toolCalls, estimatedTokens: estimateTokens(textOfTurns(turns)) }
|
|
1755
|
+
};
|
|
1756
|
+
if (summary) digest.summary = summary;
|
|
1757
|
+
return digest;
|
|
1758
|
+
}
|
|
1759
|
+
function textOfTurns(turns) {
|
|
1760
|
+
return turns.map((turn) => turn.parts.map(textOfPart).join("\n")).join("\n");
|
|
1761
|
+
}
|
|
1762
|
+
function firstText(turns, role, keep = () => true) {
|
|
1763
|
+
for (const turn of turns) {
|
|
1764
|
+
if (turn.role !== role) continue;
|
|
1765
|
+
const part = turn.parts.find((p) => p.kind === "text" && p.text.trim().length > 0 && keep(p.text.trim()));
|
|
1766
|
+
if (part && part.kind === "text") return part.text.trim();
|
|
1767
|
+
}
|
|
1768
|
+
return void 0;
|
|
1769
|
+
}
|
|
1770
|
+
function lastText(turns, role) {
|
|
1771
|
+
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
1772
|
+
const turn = turns[i];
|
|
1773
|
+
if (turn.role !== role) continue;
|
|
1774
|
+
const texts = turn.parts.filter((p) => p.kind === "text" && p.text.trim().length > 0);
|
|
1775
|
+
if (texts.length > 0) return texts.map((p) => p.text.trim()).join("\n\n");
|
|
1776
|
+
}
|
|
1777
|
+
return void 0;
|
|
1778
|
+
}
|
|
1779
|
+
function filesOf(part) {
|
|
1780
|
+
const found = [];
|
|
1781
|
+
const input = part.input;
|
|
1782
|
+
if (WRITE_TOOLS.has(part.name)) {
|
|
1783
|
+
if (typeof input === "object" && input !== null) {
|
|
1784
|
+
for (const key of PATH_KEYS) {
|
|
1785
|
+
const value = input[key];
|
|
1786
|
+
if (typeof value === "string" && value.length > 0) found.push(value);
|
|
1787
|
+
}
|
|
1788
|
+
const patch = input["patch"] ?? input["input"];
|
|
1789
|
+
if (typeof patch === "string") for (const m of patch.matchAll(PATCH_FILE)) found.push(m[1].trim());
|
|
1790
|
+
} else if (typeof input === "string") {
|
|
1791
|
+
for (const m of input.matchAll(PATCH_FILE)) found.push(m[1].trim());
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
const command = commandOf(part);
|
|
1795
|
+
if (command) found.push(...movedFiles(command));
|
|
1796
|
+
return found;
|
|
1797
|
+
}
|
|
1798
|
+
function movedFiles(command) {
|
|
1799
|
+
const found = [];
|
|
1800
|
+
for (const segment of command.split(/\s*(?:&&|\|\||;|\|)\s*/)) {
|
|
1801
|
+
const words = shellWords(segment);
|
|
1802
|
+
const verb = words[0];
|
|
1803
|
+
if (verb !== "mv" && verb !== "cp" && verb !== "touch") continue;
|
|
1804
|
+
for (const word of words.slice(1)) {
|
|
1805
|
+
if (word.startsWith("-") || word.length === 0 || /^\\+$/.test(word)) continue;
|
|
1806
|
+
found.push(word);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return found;
|
|
1810
|
+
}
|
|
1811
|
+
function shellWords(segment) {
|
|
1812
|
+
const words = [];
|
|
1813
|
+
let current = "";
|
|
1814
|
+
let quote;
|
|
1815
|
+
let has = false;
|
|
1816
|
+
for (const char of segment) {
|
|
1817
|
+
if (quote) {
|
|
1818
|
+
if (char === quote) quote = void 0;
|
|
1819
|
+
else current += char;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (char === "'" || char === '"') {
|
|
1823
|
+
quote = char;
|
|
1824
|
+
has = true;
|
|
1825
|
+
} else if (/\s/.test(char)) {
|
|
1826
|
+
if (has) words.push(current);
|
|
1827
|
+
current = "";
|
|
1828
|
+
has = false;
|
|
1829
|
+
} else {
|
|
1830
|
+
current += char;
|
|
1831
|
+
has = true;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (has) words.push(current);
|
|
1835
|
+
return words;
|
|
1836
|
+
}
|
|
1837
|
+
function commandOf(part) {
|
|
1838
|
+
if (!SHELL_TOOLS.has(part.name)) return void 0;
|
|
1839
|
+
const input = part.input;
|
|
1840
|
+
if (typeof input === "string") return commandInScript(input);
|
|
1841
|
+
if (typeof input !== "object" || input === null) return void 0;
|
|
1842
|
+
const record2 = input;
|
|
1843
|
+
const direct = record2["command"] ?? record2["cmd"];
|
|
1844
|
+
if (typeof direct === "string") return oneLine(direct);
|
|
1845
|
+
if (Array.isArray(direct)) return oneLine(direct.map(String).join(" "));
|
|
1846
|
+
return void 0;
|
|
1847
|
+
}
|
|
1848
|
+
function commandInScript(script) {
|
|
1849
|
+
const match = /"cmd"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(script);
|
|
1850
|
+
if (!match) return void 0;
|
|
1851
|
+
try {
|
|
1852
|
+
return oneLine(JSON.parse(`"${match[1]}"`));
|
|
1853
|
+
} catch {
|
|
1854
|
+
return void 0;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
function openItemsOf(text) {
|
|
1858
|
+
const items = [];
|
|
1859
|
+
let underHeading = false;
|
|
1860
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
1861
|
+
const line2 = raw.trim();
|
|
1862
|
+
if (line2.length === 0) {
|
|
1863
|
+
underHeading = false;
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
if (OPEN_ITEM_HEADING.test(line2)) {
|
|
1867
|
+
underHeading = true;
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const listed = /^(?:[-*•]|\d+[.)])\s+/.test(line2);
|
|
1871
|
+
if (underHeading && listed || OPEN_ITEM_LINE.test(line2)) {
|
|
1872
|
+
items.push(cutLine(line2.replace(/^(?:[-*•]|\d+[.)])\s+/, ""), LINE_CHARS));
|
|
1873
|
+
if (items.length >= MAX_OPEN_ITEMS) break;
|
|
1874
|
+
} else if (!listed) {
|
|
1875
|
+
underHeading = false;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return items;
|
|
1879
|
+
}
|
|
1880
|
+
function oneLine(text) {
|
|
1881
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1882
|
+
}
|
|
1883
|
+
function firstLine(text) {
|
|
1884
|
+
return text.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
1885
|
+
}
|
|
1886
|
+
function cutLine(text, max) {
|
|
1887
|
+
const line2 = oneLine(text);
|
|
1888
|
+
return line2.length > max ? `${line2.slice(0, max - 1).trimEnd()}\u2026` : line2;
|
|
1889
|
+
}
|
|
1890
|
+
function cut2(text, max) {
|
|
1891
|
+
const clean = text.trim();
|
|
1892
|
+
return clean.length > max ? `${clean.slice(0, max - 1).trimEnd()}\u2026` : clean;
|
|
1893
|
+
}
|
|
1894
|
+
function digestMarkdown(digest) {
|
|
1895
|
+
const lines = [`# ${digest.title}`, ""];
|
|
1896
|
+
if (digest.goal) lines.push("## Goal", "", digest.goal, "");
|
|
1897
|
+
if (digest.summary) lines.push("## Summary", "", digest.summary.trim(), "");
|
|
1898
|
+
if (digest.decisions.length > 0) lines.push("## Decisions", "", ...digest.decisions.map((d) => `- ${d}`), "");
|
|
1899
|
+
if (digest.filesTouched.length > 0) lines.push("## Files touched", "", ...digest.filesTouched.map((f) => `- \`${f}\``), "");
|
|
1900
|
+
if (digest.commandsRun.length > 0) lines.push("## Commands run", "", ...digest.commandsRun.map((c) => `- \`${c}\``), "");
|
|
1901
|
+
if (digest.openItems.length > 0) lines.push("## Open items", "", ...digest.openItems.map((o) => `- ${o}`), "");
|
|
1902
|
+
if (digest.lastExchange.user || digest.lastExchange.assistant) {
|
|
1903
|
+
lines.push("## Last exchange", "");
|
|
1904
|
+
if (digest.lastExchange.user) lines.push(`**User:** ${digest.lastExchange.user}`, "");
|
|
1905
|
+
if (digest.lastExchange.assistant) lines.push(`**Assistant:** ${digest.lastExchange.assistant}`, "");
|
|
1906
|
+
}
|
|
1907
|
+
const { turns, toolCalls, estimatedTokens } = digest.stats;
|
|
1908
|
+
lines.push(`_Digest by ${digest.by} \xB7 tool calls: ${toolCalls} \xB7 tokens (estimated): ${estimatedTokens} \xB7 turns: ${turns}_`);
|
|
1909
|
+
return `${lines.join("\n")}
|
|
1910
|
+
`;
|
|
1911
|
+
}
|
|
1912
|
+
function compactConversation(conversation, digest, options = {}) {
|
|
1913
|
+
const keep = Math.max(1, Math.floor(options.keepTurns ?? KEEP_TURNS_DEFAULT));
|
|
1914
|
+
const source = conversation.turns;
|
|
1915
|
+
let start = Math.max(0, source.length - keep);
|
|
1916
|
+
while (start < source.length && source[start].parts.every((part) => part.kind === "summary")) start += 1;
|
|
1917
|
+
const first = source[start];
|
|
1918
|
+
if (start > 0 && first && first.role === "user" && first.parts.some((part) => part.kind === "tool_result")) {
|
|
1919
|
+
start -= 1;
|
|
1920
|
+
}
|
|
1921
|
+
const kept = source.slice(start).map((turn) => ({ ...turn, parts: [...turn.parts] }));
|
|
1922
|
+
const summary = { role: "user", parts: [{ kind: "summary", text: digestMarkdown(digest) }] };
|
|
1923
|
+
const at = kept[0]?.at ?? conversation.startedAt;
|
|
1924
|
+
if (at) summary.at = at;
|
|
1925
|
+
const turns = [summary, ...kept];
|
|
1926
|
+
return {
|
|
1927
|
+
...conversation,
|
|
1928
|
+
turns,
|
|
1929
|
+
turnCount: turns.length,
|
|
1930
|
+
compacted: true,
|
|
1931
|
+
hash: hashTurns(turns),
|
|
1932
|
+
compactions: [...conversation.compactions],
|
|
1933
|
+
dropped: { ...conversation.dropped }
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
var Redactor = class {
|
|
1937
|
+
count = 0;
|
|
1938
|
+
text(value) {
|
|
1939
|
+
const out = redactSecrets(value);
|
|
1940
|
+
if (out !== value) this.count += marks(out) - marks(value);
|
|
1941
|
+
return out;
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
function marks(text) {
|
|
1945
|
+
return text.split(REDACTED).length - 1;
|
|
1946
|
+
}
|
|
1947
|
+
function withProvenance(turns, request) {
|
|
1948
|
+
const { conversation, tier, now } = request;
|
|
1949
|
+
const line2 = provenanceLine(conversation.agent, conversation.sessionId, tier, now);
|
|
1950
|
+
const copy = turns.map((turn) => ({ ...turn, parts: turn.parts.map((part2) => ({ ...part2 })) }));
|
|
1951
|
+
const first = copy[0];
|
|
1952
|
+
const part = first?.parts[0];
|
|
1953
|
+
if (first && first.role === "user" && part && (part.kind === "text" || part.kind === "summary")) {
|
|
1954
|
+
part.text = `${line2}
|
|
1955
|
+
|
|
1956
|
+
${part.text}`;
|
|
1957
|
+
return copy;
|
|
1958
|
+
}
|
|
1959
|
+
return [{ role: "user", parts: [{ kind: "text", text: line2 }] }, ...copy];
|
|
1960
|
+
}
|
|
1961
|
+
function balanceTurns(turns, dropped) {
|
|
1962
|
+
const out = [];
|
|
1963
|
+
const open2 = /* @__PURE__ */ new Set();
|
|
1964
|
+
for (const turn of turns) {
|
|
1965
|
+
const parts = [];
|
|
1966
|
+
for (const part of turn.parts) {
|
|
1967
|
+
if (part.kind === "tool_result") {
|
|
1968
|
+
if (!open2.has(part.callId)) {
|
|
1969
|
+
dropped.other += 1;
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
open2.delete(part.callId);
|
|
1973
|
+
}
|
|
1974
|
+
if (part.kind === "tool_call") open2.add(part.id);
|
|
1975
|
+
parts.push(part);
|
|
1976
|
+
}
|
|
1977
|
+
if (turn.role === "user" && open2.size > 0) {
|
|
1978
|
+
const missing = [...open2].map((callId) => ({ kind: "tool_result", callId, output: "(result not carried)" }));
|
|
1979
|
+
open2.clear();
|
|
1980
|
+
parts.unshift(...missing);
|
|
1981
|
+
}
|
|
1982
|
+
if (parts.length > 0) out.push({ ...turn, parts });
|
|
1983
|
+
}
|
|
1984
|
+
if (open2.size > 0) {
|
|
1985
|
+
out.push({
|
|
1986
|
+
role: "user",
|
|
1987
|
+
parts: [...open2].map((callId) => ({ kind: "tool_result", callId, output: "(result not carried)" }))
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
return out;
|
|
1991
|
+
}
|
|
1992
|
+
async function writeAtomic(path, content, dir, id, mode = 384) {
|
|
1993
|
+
const temp = nativePath().join(dir, `.panoma-${id}.tmp`);
|
|
1994
|
+
try {
|
|
1995
|
+
await mkdir(dir, { recursive: true });
|
|
1996
|
+
await writeFile(temp, content, { encoding: "utf8", mode });
|
|
1997
|
+
await rename(temp, path);
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
await rm(temp, { force: true }).catch(() => void 0);
|
|
2000
|
+
throw asHandoffFault(error, "write-failed");
|
|
2001
|
+
}
|
|
2002
|
+
return Buffer.byteLength(content, "utf8");
|
|
2003
|
+
}
|
|
2004
|
+
function resultOf(agent, request, sessionId, path, turns, bytes, redactor, steps = []) {
|
|
2005
|
+
const { conversation, tier, now, cwd, platform } = request;
|
|
2006
|
+
const surface = request.surface ?? "cli";
|
|
2007
|
+
return {
|
|
2008
|
+
agent,
|
|
2009
|
+
surface,
|
|
2010
|
+
sessionId,
|
|
2011
|
+
path,
|
|
2012
|
+
resume: resumeOf(agent, sessionId, cwd, platform),
|
|
2013
|
+
resumeInApp: resumeInApp(agent, sessionId, cwd, platform),
|
|
2014
|
+
steps,
|
|
2015
|
+
fidelity: fidelityOf(agent),
|
|
2016
|
+
provenance: {
|
|
2017
|
+
sourceAgent: conversation.agent,
|
|
2018
|
+
sourceSessionId: conversation.sessionId,
|
|
2019
|
+
sourceHash: conversation.hash,
|
|
2020
|
+
tier,
|
|
2021
|
+
at: now.toISOString(),
|
|
2022
|
+
by: "panoma"
|
|
2023
|
+
},
|
|
2024
|
+
turns,
|
|
2025
|
+
bytes,
|
|
2026
|
+
dropped: { ...conversation.dropped, secrets: conversation.dropped.secrets + redactor.count }
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
function clock(now) {
|
|
2030
|
+
let ms = now.getTime();
|
|
2031
|
+
return () => new Date(ms++);
|
|
2032
|
+
}
|
|
2033
|
+
function briefMarkdown(conversation, digest, options = {}) {
|
|
2034
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
2035
|
+
const redactor = options.redactor ?? new Redactor();
|
|
2036
|
+
const keep = Math.max(0, Math.floor(options.keepTurns ?? KEEP_TURNS_DEFAULT));
|
|
2037
|
+
const recent = keep > 0 ? conversation.turns.slice(-keep) : [];
|
|
2038
|
+
const lines = [
|
|
2039
|
+
provenanceLine(conversation.agent, conversation.sessionId, "brief", now),
|
|
2040
|
+
"",
|
|
2041
|
+
redactor.text(digestMarkdown(digest)).trimEnd(),
|
|
2042
|
+
""
|
|
2043
|
+
];
|
|
2044
|
+
if (recent.length > 0) {
|
|
2045
|
+
lines.push(`## Last turns (${AGENT_NAMES[conversation.agent]})`, "");
|
|
2046
|
+
for (const turn of recent) lines.push(...renderTurn(turn, redactor), "");
|
|
2047
|
+
}
|
|
2048
|
+
lines.push(
|
|
2049
|
+
"---",
|
|
2050
|
+
"",
|
|
2051
|
+
"This document was written by panoma from a conversation kept by another agent. Continue the work from here; the original transcript was not modified."
|
|
2052
|
+
);
|
|
2053
|
+
return `${lines.join("\n")}
|
|
2054
|
+
`;
|
|
2055
|
+
}
|
|
2056
|
+
function renderTurn(turn, redactor) {
|
|
2057
|
+
const who = turn.role === "user" ? "User" : "Assistant";
|
|
2058
|
+
const body = turn.parts.map((part) => part.kind === "text" || part.kind === "summary" ? part.text : `\`\`\`
|
|
2059
|
+
${textOfPart(part)}
|
|
2060
|
+
\`\`\``).join("\n\n");
|
|
2061
|
+
return [`### ${who}`, "", redactor.text(body).trimEnd()];
|
|
2062
|
+
}
|
|
2063
|
+
async function writeBrief(path, markdown) {
|
|
2064
|
+
try {
|
|
2065
|
+
await mkdir2(nativePath().dirname(path), { recursive: true });
|
|
2066
|
+
await writeFile2(path, markdown, { encoding: "utf8", mode: 384 });
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
throw asHandoffFault(error, "write-failed");
|
|
2069
|
+
}
|
|
2070
|
+
return Buffer.byteLength(markdown, "utf8");
|
|
2071
|
+
}
|
|
2072
|
+
var CLAUDE_RECORD_VERSION = HANDOFF_CLAUDE_RECORD_VERSION;
|
|
2073
|
+
async function writeClaudeConversation(request) {
|
|
2074
|
+
const { conversation, cwd, gitBranch, random, now, platform, title } = request;
|
|
2075
|
+
const store = claudeStoreAt(request.root, { home: request.root, env: {}, platform });
|
|
2076
|
+
const sessionId = random.uuid();
|
|
2077
|
+
const dir = claudeProjectDir(store, cwd);
|
|
2078
|
+
const path = store.resolved.path.join(dir, `${sessionId}.jsonl`);
|
|
2079
|
+
const redactor = new Redactor();
|
|
2080
|
+
const dropped = { ...conversation.dropped };
|
|
2081
|
+
const turns = balanceTurns(withProvenance(conversation.turns, request), dropped);
|
|
2082
|
+
const tick = clock(now);
|
|
2083
|
+
const records = [];
|
|
2084
|
+
let parent = null;
|
|
2085
|
+
const envelope = (at) => {
|
|
2086
|
+
const base = {
|
|
2087
|
+
parentUuid: parent,
|
|
2088
|
+
isSidechain: false,
|
|
2089
|
+
userType: "external",
|
|
2090
|
+
cwd,
|
|
2091
|
+
sessionId,
|
|
2092
|
+
version: CLAUDE_RECORD_VERSION,
|
|
2093
|
+
timestamp: at ?? tick().toISOString()
|
|
2094
|
+
};
|
|
2095
|
+
if (gitBranch) base["gitBranch"] = gitBranch;
|
|
2096
|
+
return base;
|
|
2097
|
+
};
|
|
2098
|
+
const push = (record2) => {
|
|
2099
|
+
const uuid = random.uuid();
|
|
2100
|
+
records.push({ ...record2, uuid });
|
|
2101
|
+
parent = uuid;
|
|
2102
|
+
};
|
|
2103
|
+
const shownTitle = title ?? conversation.title;
|
|
2104
|
+
if (shownTitle) {
|
|
2105
|
+
records.push({ type: "custom-title", customTitle: redactor.text(shownTitle), sessionId });
|
|
2106
|
+
}
|
|
2107
|
+
for (const turn of turns) {
|
|
2108
|
+
if (turn.role === "user") {
|
|
2109
|
+
const rest = [];
|
|
2110
|
+
for (const part of turn.parts) {
|
|
2111
|
+
if (part.kind !== "summary") {
|
|
2112
|
+
rest.push(part);
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
const boundaryAt = turn.at ?? tick().toISOString();
|
|
2116
|
+
const boundaryUuid = random.uuid();
|
|
2117
|
+
records.push({
|
|
2118
|
+
parentUuid: null,
|
|
2119
|
+
logicalParentUuid: parent,
|
|
2120
|
+
isSidechain: false,
|
|
2121
|
+
type: "system",
|
|
2122
|
+
subtype: "compact_boundary",
|
|
2123
|
+
content: "Conversation compacted",
|
|
2124
|
+
isMeta: false,
|
|
2125
|
+
level: "info",
|
|
2126
|
+
compactMetadata: { trigger: "manual", preTokens: 0, postTokens: 0 },
|
|
2127
|
+
uuid: boundaryUuid,
|
|
2128
|
+
timestamp: boundaryAt,
|
|
2129
|
+
sessionId,
|
|
2130
|
+
cwd,
|
|
2131
|
+
version: CLAUDE_RECORD_VERSION
|
|
2132
|
+
});
|
|
2133
|
+
parent = boundaryUuid;
|
|
2134
|
+
push({
|
|
2135
|
+
...envelope(turn.at),
|
|
2136
|
+
type: "user",
|
|
2137
|
+
message: { role: "user", content: redactor.text(part.text) },
|
|
2138
|
+
isCompactSummary: true,
|
|
2139
|
+
isVisibleInTranscriptOnly: true
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
if (rest.length === 0) continue;
|
|
2143
|
+
push({ ...envelope(turn.at), type: "user", message: { role: "user", content: userContent(rest, redactor) } });
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const messageId = `msg_${random.hex(12)}`;
|
|
2147
|
+
const blocks = assistantBlocks(turn.parts, redactor);
|
|
2148
|
+
if (blocks.length === 0) continue;
|
|
2149
|
+
blocks.forEach((block, index) => {
|
|
2150
|
+
push({
|
|
2151
|
+
...envelope(turn.at),
|
|
2152
|
+
type: "assistant",
|
|
2153
|
+
message: {
|
|
2154
|
+
id: messageId,
|
|
2155
|
+
type: "message",
|
|
2156
|
+
role: "assistant",
|
|
2157
|
+
content: [block],
|
|
2158
|
+
stop_reason: index === blocks.length - 1 ? block["type"] === "tool_use" ? "tool_use" : "end_turn" : null,
|
|
2159
|
+
stop_sequence: null
|
|
2160
|
+
},
|
|
2161
|
+
apiBlockIndex: index
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
const content = `${records.map((record2) => JSON.stringify(record2)).join("\n")}
|
|
2166
|
+
`;
|
|
2167
|
+
const bytes = await writeAtomic(path, content, dir, sessionId);
|
|
2168
|
+
const result = resultOf("claude-cli", request, sessionId, path, turns.length, bytes, redactor);
|
|
2169
|
+
result.dropped = { ...dropped, secrets: dropped.secrets + redactor.count };
|
|
2170
|
+
return result;
|
|
2171
|
+
}
|
|
2172
|
+
function userContent(parts, redactor) {
|
|
2173
|
+
if (parts.length === 1 && parts[0].kind === "text") return redactor.text(parts[0].text);
|
|
2174
|
+
const blocks = [];
|
|
2175
|
+
for (const part of parts) {
|
|
2176
|
+
if (part.kind === "text") blocks.push({ type: "text", text: redactor.text(part.text) });
|
|
2177
|
+
else if (part.kind === "tool_result") {
|
|
2178
|
+
const block = {
|
|
2179
|
+
tool_use_id: part.callId,
|
|
2180
|
+
type: "tool_result",
|
|
2181
|
+
content: redactor.text(part.output)
|
|
2182
|
+
};
|
|
2183
|
+
if (part.isError) block["is_error"] = true;
|
|
2184
|
+
blocks.push(block);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return blocks;
|
|
2188
|
+
}
|
|
2189
|
+
function assistantBlocks(parts, redactor) {
|
|
2190
|
+
const blocks = [];
|
|
2191
|
+
for (const part of parts) {
|
|
2192
|
+
if (part.kind === "text" || part.kind === "summary") {
|
|
2193
|
+
blocks.push({ type: "text", text: redactor.text(part.text) });
|
|
2194
|
+
} else if (part.kind === "tool_call") {
|
|
2195
|
+
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: toolUseInput(redactInput(part.input, redactor)) });
|
|
2196
|
+
} else {
|
|
2197
|
+
blocks.push({ type: "text", text: redactor.text(`[tool result]
|
|
2198
|
+
${part.output}`) });
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return blocks;
|
|
2202
|
+
}
|
|
2203
|
+
function toolUseInput(input) {
|
|
2204
|
+
if (typeof input === "object" && input !== null && !Array.isArray(input)) return input;
|
|
2205
|
+
return { input: input ?? "" };
|
|
2206
|
+
}
|
|
2207
|
+
function redactInput(input, redactor) {
|
|
2208
|
+
if (typeof input === "string") return redactor.text(input);
|
|
2209
|
+
if (Array.isArray(input)) return input.map((item) => redactInput(item, redactor));
|
|
2210
|
+
if (typeof input === "object" && input !== null) {
|
|
2211
|
+
const out = {};
|
|
2212
|
+
for (const [key, value] of Object.entries(input)) out[key] = redactInput(value, redactor);
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2215
|
+
return input;
|
|
2216
|
+
}
|
|
2217
|
+
var CODEX_ORIGINATOR = "panoma";
|
|
2218
|
+
var CODEX_CLI_VERSION = "0.0.0-panoma";
|
|
2219
|
+
async function writeCodexConversation(request) {
|
|
2220
|
+
const { conversation, cwd, gitBranch, random, now, platform, title } = request;
|
|
2221
|
+
const store = codexStoreAt(request.root, { home: request.root, env: {}, platform });
|
|
2222
|
+
const sessionId = random.uuid();
|
|
2223
|
+
const path = codexRolloutPath(store, now, sessionId);
|
|
2224
|
+
const dir = store.resolved.path.dirname(path);
|
|
2225
|
+
const redactor = new Redactor();
|
|
2226
|
+
const turns = withProvenance(conversation.turns, request);
|
|
2227
|
+
const tick = clock(now);
|
|
2228
|
+
const lines = [];
|
|
2229
|
+
const meta = {
|
|
2230
|
+
session_id: sessionId,
|
|
2231
|
+
id: sessionId,
|
|
2232
|
+
timestamp: now.toISOString(),
|
|
2233
|
+
cwd,
|
|
2234
|
+
originator: CODEX_ORIGINATOR,
|
|
2235
|
+
cli_version: CODEX_CLI_VERSION,
|
|
2236
|
+
source: "cli"
|
|
2237
|
+
};
|
|
2238
|
+
if (gitBranch) meta["git"] = { branch: gitBranch };
|
|
2239
|
+
lines.push({ timestamp: now.toISOString(), type: "session_meta", payload: meta });
|
|
2240
|
+
for (const turn of turns) {
|
|
2241
|
+
const at = turn.at ?? tick().toISOString();
|
|
2242
|
+
const texts = turn.parts.map((part) => redactor.text(textOfPart(part))).filter((t) => t.length > 0);
|
|
2243
|
+
if (texts.length === 0) continue;
|
|
2244
|
+
const message = turn.role === "user" ? { type: "message", role: "user", content: texts.map((text) => ({ type: "input_text", text })) } : { type: "message", role: "assistant", content: texts.map((text) => ({ type: "output_text", text })) };
|
|
2245
|
+
lines.push({ timestamp: at, type: "response_item", payload: message });
|
|
2246
|
+
lines.push({
|
|
2247
|
+
timestamp: at,
|
|
2248
|
+
type: "event_msg",
|
|
2249
|
+
payload: { type: turn.role === "user" ? "user_message" : "agent_message", message: texts.join("\n\n") }
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
lines.push(threadSettingsApplied(sessionId, cwd, await codexModelSettings(store), tick().toISOString()));
|
|
2253
|
+
const content = `${lines.map((line2) => JSON.stringify(line2)).join("\n")}
|
|
2254
|
+
`;
|
|
2255
|
+
const bytes = await writeAtomic(path, content, dir, sessionId);
|
|
2256
|
+
const shownTitle = title ?? conversation.title;
|
|
2257
|
+
if (shownTitle) {
|
|
2258
|
+
const entry = { id: sessionId, thread_name: redactor.text(shownTitle), updated_at: now.toISOString() };
|
|
2259
|
+
try {
|
|
2260
|
+
await appendFile(store.index, `${JSON.stringify(entry)}
|
|
2261
|
+
`, "utf8");
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
return resultOf("codex-cli", request, sessionId, path, countWritten(turns), bytes, redactor);
|
|
2266
|
+
}
|
|
2267
|
+
function threadSettingsApplied(sessionId, cwd, settings, at) {
|
|
2268
|
+
const { model, reasoningEffort, approvalPolicy, sandboxMode } = settings;
|
|
2269
|
+
return {
|
|
2270
|
+
timestamp: at,
|
|
2271
|
+
type: "event_msg",
|
|
2272
|
+
payload: {
|
|
2273
|
+
type: "thread_settings_applied",
|
|
2274
|
+
thread_id: sessionId,
|
|
2275
|
+
thread_settings: {
|
|
2276
|
+
model,
|
|
2277
|
+
model_provider_id: "openai",
|
|
2278
|
+
approval_policy: approvalPolicy,
|
|
2279
|
+
approvals_reviewer: "user",
|
|
2280
|
+
permission_profile: permissionProfile(sandboxMode, cwd),
|
|
2281
|
+
cwd,
|
|
2282
|
+
reasoning_effort: reasoningEffort,
|
|
2283
|
+
collaboration_mode: { mode: "default", settings: { model, reasoning_effort: reasoningEffort, developer_instructions: null } }
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function permissionProfile(sandboxMode, cwd) {
|
|
2289
|
+
if (sandboxMode === "danger-full-access") return { type: "disabled" };
|
|
2290
|
+
const entries = [{ path: { type: "special", value: { kind: "root" } }, access: "read" }];
|
|
2291
|
+
if (sandboxMode !== "read-only") entries.push({ path: { type: "path", path: cwd }, access: "write" });
|
|
2292
|
+
entries.push(
|
|
2293
|
+
{ path: { type: "special", value: { kind: "slash_tmp" } }, access: "write" },
|
|
2294
|
+
{ path: { type: "special", value: { kind: "tmpdir" } }, access: "write" }
|
|
2295
|
+
);
|
|
2296
|
+
return { type: "managed", file_system: { type: "restricted", entries } };
|
|
2297
|
+
}
|
|
2298
|
+
function countWritten(turns) {
|
|
2299
|
+
return turns.filter((turn) => turn.parts.some((part) => textOfPart(part).length > 0)).length;
|
|
2300
|
+
}
|
|
2301
|
+
var OPENCODE_PROVIDER = "panoma";
|
|
2302
|
+
var OPENCODE_MODEL = "handoff";
|
|
2303
|
+
var OPENCODE_VERSION = "0.0.0-panoma";
|
|
2304
|
+
async function writeOpencodeConversation(request) {
|
|
2305
|
+
const { conversation, cwd, random, now, platform, title } = request;
|
|
2306
|
+
const store = opencodeStoreAt(request.root, { home: request.root, env: {}, platform });
|
|
2307
|
+
const redactor = new Redactor();
|
|
2308
|
+
const turns = withProvenance(conversation.turns, request);
|
|
2309
|
+
const tick = clock(now);
|
|
2310
|
+
let counter = 0;
|
|
2311
|
+
const nextId = (prefix, at) => opencodeId(prefix, at, counter++, random);
|
|
2312
|
+
const sessionId = nextId("ses", now);
|
|
2313
|
+
const path = opencodeEnvelopePath(store, sessionId);
|
|
2314
|
+
const shownTitle = redactor.text(title ?? conversation.title ?? "Handed off by panoma");
|
|
2315
|
+
const messages = [];
|
|
2316
|
+
let lastUser;
|
|
2317
|
+
let written = 0;
|
|
2318
|
+
const userMessage = (at, parts, id) => {
|
|
2319
|
+
lastUser = id;
|
|
2320
|
+
messages.push({
|
|
2321
|
+
info: {
|
|
2322
|
+
id,
|
|
2323
|
+
sessionID: sessionId,
|
|
2324
|
+
role: "user",
|
|
2325
|
+
time: { created: at.getTime() },
|
|
2326
|
+
agent: "build",
|
|
2327
|
+
model: { providerID: OPENCODE_PROVIDER, modelID: OPENCODE_MODEL }
|
|
2328
|
+
},
|
|
2329
|
+
parts
|
|
2330
|
+
});
|
|
2331
|
+
};
|
|
2332
|
+
const assistantMessage = (at, parts, id, summary) => {
|
|
2333
|
+
const info = {
|
|
2334
|
+
id,
|
|
2335
|
+
sessionID: sessionId,
|
|
2336
|
+
role: "assistant",
|
|
2337
|
+
time: { created: at.getTime(), completed: at.getTime() },
|
|
2338
|
+
parentID: lastUser ?? id,
|
|
2339
|
+
modelID: OPENCODE_MODEL,
|
|
2340
|
+
providerID: OPENCODE_PROVIDER,
|
|
2341
|
+
mode: summary ? "compaction" : "build",
|
|
2342
|
+
agent: summary ? "compaction" : "build",
|
|
2343
|
+
path: { cwd, root: cwd },
|
|
2344
|
+
cost: 0,
|
|
2345
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
2346
|
+
finish: "stop"
|
|
2347
|
+
};
|
|
2348
|
+
if (summary) info["summary"] = true;
|
|
2349
|
+
messages.push({ info, parts });
|
|
2350
|
+
};
|
|
2351
|
+
const textPart = (messageId, at, text) => ({
|
|
2352
|
+
id: nextId("prt", at),
|
|
2353
|
+
sessionID: sessionId,
|
|
2354
|
+
messageID: messageId,
|
|
2355
|
+
type: "text",
|
|
2356
|
+
text,
|
|
2357
|
+
time: { start: at.getTime(), end: at.getTime() }
|
|
2358
|
+
});
|
|
2359
|
+
for (const turn of turns) {
|
|
2360
|
+
const at = turn.at ? new Date(turn.at) : tick();
|
|
2361
|
+
const when = Number.isNaN(at.getTime()) ? tick() : at;
|
|
2362
|
+
const summary = turn.parts.find((part) => part.kind === "summary");
|
|
2363
|
+
if (summary) {
|
|
2364
|
+
const compactionId = nextId("msg", when);
|
|
2365
|
+
userMessage(when, [{
|
|
2366
|
+
id: nextId("prt", when),
|
|
2367
|
+
sessionID: sessionId,
|
|
2368
|
+
messageID: compactionId,
|
|
2369
|
+
type: "compaction",
|
|
2370
|
+
auto: false
|
|
2371
|
+
}], compactionId);
|
|
2372
|
+
const summaryId = nextId("msg", when);
|
|
2373
|
+
assistantMessage(when, [textPart(summaryId, when, redactor.text(summary.text))], summaryId, true);
|
|
2374
|
+
written += 1;
|
|
2375
|
+
}
|
|
2376
|
+
const texts = turn.parts.filter((part) => part.kind !== "summary").map((part) => redactor.text(textOfPart(part))).filter((text) => text.length > 0);
|
|
2377
|
+
if (texts.length === 0) continue;
|
|
2378
|
+
const id = nextId("msg", when);
|
|
2379
|
+
const parts = texts.map((text) => textPart(id, when, text));
|
|
2380
|
+
if (turn.role === "user") userMessage(when, parts, id);
|
|
2381
|
+
else assistantMessage(when, parts, id, false);
|
|
2382
|
+
written += 1;
|
|
2383
|
+
}
|
|
2384
|
+
const envelope = {
|
|
2385
|
+
info: {
|
|
2386
|
+
id: sessionId,
|
|
2387
|
+
slug: `handoff-${sessionId.slice(-6).toLowerCase()}`,
|
|
2388
|
+
projectID: "global",
|
|
2389
|
+
directory: cwd,
|
|
2390
|
+
title: shownTitle,
|
|
2391
|
+
version: OPENCODE_VERSION,
|
|
2392
|
+
time: { created: now.getTime(), updated: tick().getTime() }
|
|
2393
|
+
},
|
|
2394
|
+
messages
|
|
2395
|
+
};
|
|
2396
|
+
const content = `${JSON.stringify(envelope, null, 2)}
|
|
2397
|
+
`;
|
|
2398
|
+
const bytes = await writeAtomic(path, content, store.root, sessionId, 384);
|
|
2399
|
+
const steps = [`opencode import ${quoteForShell(path, platform)}`];
|
|
2400
|
+
return resultOf("opencode", request, sessionId, path, written, bytes, redactor, steps);
|
|
2401
|
+
}
|
|
2402
|
+
async function writeGeminiConversation(request) {
|
|
2403
|
+
const { conversation, cwd, random, now, platform, title } = request;
|
|
2404
|
+
const store = geminiStoreAt(request.root, { home: request.root, env: {}, platform });
|
|
2405
|
+
const sessionId = random.uuid();
|
|
2406
|
+
const projectId = geminiProjectId(store, cwd);
|
|
2407
|
+
const path = geminiChatPath(store, projectId, now, sessionId);
|
|
2408
|
+
const dir = store.resolved.path.dirname(path);
|
|
2409
|
+
const redactor = new Redactor();
|
|
2410
|
+
const turns = withProvenance(conversation.turns, request);
|
|
2411
|
+
const tick = clock(now);
|
|
2412
|
+
const lines = [];
|
|
2413
|
+
let written = 0;
|
|
2414
|
+
let last = now.toISOString();
|
|
2415
|
+
for (const turn of turns) {
|
|
2416
|
+
const at = turn.at ?? tick().toISOString();
|
|
2417
|
+
const texts = turn.parts.map((part) => redactor.text(textOfPart(part))).filter((t) => t.length > 0);
|
|
2418
|
+
if (texts.length === 0) continue;
|
|
2419
|
+
lines.push({
|
|
2420
|
+
id: random.uuid(),
|
|
2421
|
+
timestamp: at,
|
|
2422
|
+
type: turn.role === "user" ? "user" : "gemini",
|
|
2423
|
+
content: texts.map((text) => ({ text }))
|
|
2424
|
+
});
|
|
2425
|
+
written += 1;
|
|
2426
|
+
last = at;
|
|
2427
|
+
}
|
|
2428
|
+
lines.unshift({
|
|
2429
|
+
sessionId,
|
|
2430
|
+
projectHash: projectId,
|
|
2431
|
+
startTime: turns[0]?.at ?? now.toISOString(),
|
|
2432
|
+
lastUpdated: last,
|
|
2433
|
+
kind: "main"
|
|
2434
|
+
});
|
|
2435
|
+
const shownTitle = title ?? conversation.title;
|
|
2436
|
+
if (shownTitle) lines.push({ $set: { summary: redactor.text(shownTitle) } });
|
|
2437
|
+
const content = `${lines.map((line2) => JSON.stringify(line2)).join("\n")}
|
|
2438
|
+
`;
|
|
2439
|
+
const bytes = await writeAtomic(path, content, dir, sessionId);
|
|
2440
|
+
return resultOf("gemini-cli", request, sessionId, path, written, bytes, redactor);
|
|
2441
|
+
}
|
|
2442
|
+
async function scopeOf(cwd, platform) {
|
|
2443
|
+
if (!cwd) return void 0;
|
|
2444
|
+
const given = cwd.replace(/[\\/]+$/, "") || cwd;
|
|
2445
|
+
const real = await realFolder(given);
|
|
2446
|
+
return { folders: [.../* @__PURE__ */ new Set([given, real])], platform };
|
|
2447
|
+
}
|
|
2448
|
+
function inScope(scope, cwd) {
|
|
2449
|
+
return scope.folders.some((folder) => insideFolder(cwd, folder, scope.platform));
|
|
2450
|
+
}
|
|
2451
|
+
async function discoverConversations(options = {}) {
|
|
2452
|
+
const resolved = resolveStoreOptions(options);
|
|
2453
|
+
const limit = Math.max(1, Math.floor(options.limit ?? DISCOVERY_LIMIT_DEFAULT));
|
|
2454
|
+
const agents = options.agents ?? NATIVE_AGENTS;
|
|
2455
|
+
const scope = await scopeOf(options.cwd, resolved.platform);
|
|
2456
|
+
const conversations = [];
|
|
2457
|
+
const stores2 = [];
|
|
2458
|
+
for (const agent of agents) {
|
|
2459
|
+
const listed = await listStore(agent, options, limit, scope);
|
|
2460
|
+
if (!listed) continue;
|
|
2461
|
+
stores2.push(listed.report);
|
|
2462
|
+
conversations.push(...listed.refs);
|
|
2463
|
+
}
|
|
2464
|
+
const filtered = scope ? conversations.filter((ref) => inScope(scope, ref.cwd)) : conversations;
|
|
2465
|
+
filtered.sort((a, b) => a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : a.id.localeCompare(b.id));
|
|
2466
|
+
return { conversations: filtered, stores: stores2 };
|
|
2467
|
+
}
|
|
2468
|
+
async function listStore(agent, options, limit, scope) {
|
|
2469
|
+
switch (agent) {
|
|
2470
|
+
case "claude-cli":
|
|
2471
|
+
return listClaude(options, limit, scope);
|
|
2472
|
+
case "codex-cli":
|
|
2473
|
+
return listCodex(options, limit, scope);
|
|
2474
|
+
case "opencode":
|
|
2475
|
+
return listOpencode(options, limit, scope);
|
|
2476
|
+
case "gemini-cli":
|
|
2477
|
+
return listGemini(options, limit, scope);
|
|
2478
|
+
default:
|
|
2479
|
+
return void 0;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
function newest(candidates, limit) {
|
|
2483
|
+
return candidates.sort((a, b) => b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path)).slice(0, limit);
|
|
2484
|
+
}
|
|
2485
|
+
async function withinScope(candidates, scope, placed) {
|
|
2486
|
+
const kept = [];
|
|
2487
|
+
for (const candidate of candidates) {
|
|
2488
|
+
let place;
|
|
2489
|
+
try {
|
|
2490
|
+
place = placed(await readHead(candidate));
|
|
2491
|
+
} catch {
|
|
2492
|
+
place = void 0;
|
|
2493
|
+
}
|
|
2494
|
+
if (place === null) continue;
|
|
2495
|
+
if (place === void 0 || inScope(scope, place)) kept.push(candidate);
|
|
2496
|
+
}
|
|
2497
|
+
return kept;
|
|
2498
|
+
}
|
|
2499
|
+
async function readHead(candidate) {
|
|
2500
|
+
const handle = await open(candidate.path, "r");
|
|
2501
|
+
try {
|
|
2502
|
+
const size2 = Math.min(candidate.bytes, DISCOVERY_WINDOW_BYTES);
|
|
2503
|
+
const buffer = Buffer.alloc(size2);
|
|
2504
|
+
const { bytesRead } = await handle.read(buffer, 0, size2, 0);
|
|
2505
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
2506
|
+
} finally {
|
|
2507
|
+
await handle.close();
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function firstRecord(head) {
|
|
2511
|
+
const cut3 = head.indexOf("\n");
|
|
2512
|
+
try {
|
|
2513
|
+
const parsed = JSON.parse(cut3 === -1 ? head : head.slice(0, cut3));
|
|
2514
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
2515
|
+
} catch {
|
|
2516
|
+
return void 0;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
async function statFiles(dir, keep) {
|
|
2520
|
+
const local = nativePath();
|
|
2521
|
+
let entries;
|
|
2522
|
+
try {
|
|
2523
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
2524
|
+
} catch {
|
|
2525
|
+
return [];
|
|
2526
|
+
}
|
|
2527
|
+
const found = [];
|
|
2528
|
+
for (const entry of entries) {
|
|
2529
|
+
if (!entry.isFile() || !keep(entry.name)) continue;
|
|
2530
|
+
const path = local.join(dir, entry.name);
|
|
2531
|
+
const info = await stat6(path).catch(() => void 0);
|
|
2532
|
+
if (info) found.push({ path, mtimeMs: info.mtimeMs, bytes: info.size });
|
|
2533
|
+
}
|
|
2534
|
+
return found;
|
|
2535
|
+
}
|
|
2536
|
+
async function subdirs(dir) {
|
|
2537
|
+
const local = nativePath();
|
|
2538
|
+
try {
|
|
2539
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2540
|
+
return entries.filter((e) => e.isDirectory()).map((e) => local.join(dir, e.name));
|
|
2541
|
+
} catch {
|
|
2542
|
+
return [];
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
async function readWindows(candidate) {
|
|
2546
|
+
const handle = await open(candidate.path, "r");
|
|
2547
|
+
try {
|
|
2548
|
+
if (candidate.bytes <= DISCOVERY_WHOLE_FILE_BYTES) {
|
|
2549
|
+
const buffer = Buffer.alloc(candidate.bytes);
|
|
2550
|
+
const { bytesRead } = await handle.read(buffer, 0, candidate.bytes, 0);
|
|
2551
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
2552
|
+
return { whole: true, head: text, tail: text };
|
|
2553
|
+
}
|
|
2554
|
+
const size2 = DISCOVERY_WINDOW_BYTES;
|
|
2555
|
+
const headBuffer = Buffer.alloc(size2);
|
|
2556
|
+
const tailBuffer = Buffer.alloc(size2);
|
|
2557
|
+
const headRead = await handle.read(headBuffer, 0, size2, 0);
|
|
2558
|
+
const tailRead = await handle.read(tailBuffer, 0, size2, candidate.bytes - size2);
|
|
2559
|
+
const head = headBuffer.subarray(0, headRead.bytesRead).toString("utf8");
|
|
2560
|
+
const tailRaw = tailBuffer.subarray(0, tailRead.bytesRead).toString("utf8");
|
|
2561
|
+
const cut3 = tailRaw.indexOf("\n");
|
|
2562
|
+
return { whole: false, head, tail: cut3 === -1 ? "" : tailRaw.slice(cut3 + 1) };
|
|
2563
|
+
} finally {
|
|
2564
|
+
await handle.close();
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
function refOf(conversation, turnCount) {
|
|
2568
|
+
const { version: _version, hash: _hash, turns: _turns, compactions: _compactions, dropped: _dropped, ...ref } = conversation;
|
|
2569
|
+
return { ...ref, turnCount };
|
|
2570
|
+
}
|
|
2571
|
+
function mergeWindows(head, tail, tailHasTitleRecord) {
|
|
2572
|
+
const ref = refOf(head, null);
|
|
2573
|
+
ref.updatedAt = tail.updatedAt > head.updatedAt ? tail.updatedAt : head.updatedAt;
|
|
2574
|
+
ref.compacted = head.compacted || tail.compacted;
|
|
2575
|
+
if (tail.limit) ref.limit = tail.limit;
|
|
2576
|
+
else delete ref.limit;
|
|
2577
|
+
if (tailHasTitleRecord && tail.title) ref.title = tail.title;
|
|
2578
|
+
return ref;
|
|
2579
|
+
}
|
|
2580
|
+
async function listClaude(options, limit, scope) {
|
|
2581
|
+
const store = claudeStore(options);
|
|
2582
|
+
const found = claudeStoreExists(store);
|
|
2583
|
+
const candidates = [];
|
|
2584
|
+
const local = nativePath();
|
|
2585
|
+
const holds = (project) => {
|
|
2586
|
+
if (!scope) return "own";
|
|
2587
|
+
const answers = scope.folders.map((folder) => claudeFolderHolds(local.basename(project), folder, scope.platform));
|
|
2588
|
+
return answers.includes("own") ? "own" : answers.includes("maybe") ? "maybe" : false;
|
|
2589
|
+
};
|
|
2590
|
+
const maybe = [];
|
|
2591
|
+
let count = 0;
|
|
2592
|
+
if (found) {
|
|
2593
|
+
for (const project of await subdirs(store.projects)) {
|
|
2594
|
+
const files = await statFiles(project, (name) => name.toLowerCase().endsWith(".jsonl"));
|
|
2595
|
+
count += files.length;
|
|
2596
|
+
const answer = holds(project);
|
|
2597
|
+
if (answer === "own") candidates.push(...files);
|
|
2598
|
+
else if (answer === "maybe") maybe.push(...files);
|
|
2599
|
+
}
|
|
2600
|
+
if (scope && maybe.length > 0) candidates.push(...await withinScope(maybe, scope, claudePlace));
|
|
2601
|
+
}
|
|
2602
|
+
const report = { agent: "claude-cli", path: store.projects, found, conversations: count };
|
|
2603
|
+
const refs = [];
|
|
2604
|
+
for (const candidate of newest(candidates, limit)) {
|
|
2605
|
+
try {
|
|
2606
|
+
const windows = await readWindows(candidate);
|
|
2607
|
+
const input = {
|
|
2608
|
+
path: candidate.path,
|
|
2609
|
+
sessionId: local.basename(candidate.path, ".jsonl"),
|
|
2610
|
+
bytes: candidate.bytes,
|
|
2611
|
+
mtime: new Date(candidate.mtimeMs).toISOString()
|
|
2612
|
+
};
|
|
2613
|
+
if (windows.whole) {
|
|
2614
|
+
const conversation = await parseClaudeTranscript(windows.head, input);
|
|
2615
|
+
refs.push(refOf(conversation, conversation.turnCount));
|
|
2616
|
+
} else {
|
|
2617
|
+
const head = await parseClaudeTranscript(windows.head, input);
|
|
2618
|
+
const tail = await parseClaudeTranscript(windows.tail, input);
|
|
2619
|
+
const titled = windows.tail.includes('"custom-title"') || windows.tail.includes('"ai-title"');
|
|
2620
|
+
refs.push(mergeWindows(head, tail, titled));
|
|
2621
|
+
}
|
|
2622
|
+
} catch {
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return { report, refs };
|
|
2626
|
+
}
|
|
2627
|
+
function claudePlace(head) {
|
|
2628
|
+
for (const line2 of head.split("\n")) {
|
|
2629
|
+
if (!line2.includes('"cwd"')) continue;
|
|
2630
|
+
let record2;
|
|
2631
|
+
try {
|
|
2632
|
+
record2 = JSON.parse(line2);
|
|
2633
|
+
} catch {
|
|
2634
|
+
continue;
|
|
2635
|
+
}
|
|
2636
|
+
if (!isRecord(record2) || record2["type"] !== "user" && record2["type"] !== "assistant") continue;
|
|
2637
|
+
const cwd = readString(record2["cwd"]);
|
|
2638
|
+
if (cwd) return cwd;
|
|
2639
|
+
}
|
|
2640
|
+
return void 0;
|
|
2641
|
+
}
|
|
2642
|
+
async function listCodex(options, limit, scope) {
|
|
2643
|
+
const store = codexStore(options);
|
|
2644
|
+
const found = codexStoreExists(store);
|
|
2645
|
+
const candidates = [];
|
|
2646
|
+
if (found) {
|
|
2647
|
+
for (const year of await subdirs(store.sessions)) {
|
|
2648
|
+
for (const month of await subdirs(year)) {
|
|
2649
|
+
for (const day of await subdirs(month)) {
|
|
2650
|
+
candidates.push(...await statFiles(day, (name) => codexIdFromName(name) !== void 0));
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
const perThread = /* @__PURE__ */ new Map();
|
|
2656
|
+
const local = nativePath();
|
|
2657
|
+
for (const candidate of candidates) {
|
|
2658
|
+
const id = codexIdFromName(local.basename(candidate.path)) ?? candidate.path;
|
|
2659
|
+
const seen = perThread.get(id);
|
|
2660
|
+
if (!seen || seen.mtimeMs < candidate.mtimeMs) perThread.set(id, candidate);
|
|
2661
|
+
}
|
|
2662
|
+
const threads = [...perThread.values()];
|
|
2663
|
+
const report = { agent: "codex-cli", path: store.sessions, found, conversations: threads.length };
|
|
2664
|
+
const refs = [];
|
|
2665
|
+
const chosen = newest(scope ? await withinScope(threads, scope, codexPlace) : threads, limit);
|
|
2666
|
+
const names = chosen.length > 0 ? await readSessionIndex(store.index) : /* @__PURE__ */ new Map();
|
|
2667
|
+
for (const candidate of chosen) {
|
|
2668
|
+
try {
|
|
2669
|
+
const windows = await readWindows(candidate);
|
|
2670
|
+
const input = { path: candidate.path, bytes: candidate.bytes, mtime: new Date(candidate.mtimeMs).toISOString(), names };
|
|
2671
|
+
if (windows.whole) {
|
|
2672
|
+
const conversation = parseCodexRollout(windows.head, input);
|
|
2673
|
+
if (isSubagentRollout(windows.head)) continue;
|
|
2674
|
+
refs.push(refOf(conversation, conversation.turnCount));
|
|
2675
|
+
} else {
|
|
2676
|
+
if (isSubagentRollout(windows.head)) continue;
|
|
2677
|
+
const head = parseCodexRollout(windows.head, input);
|
|
2678
|
+
const tail = parseCodexRollout(windows.tail, input);
|
|
2679
|
+
const ref = mergeWindows(head, tail, false);
|
|
2680
|
+
ref.compacted = ref.compacted || windows.tail.includes('"type":"compacted"');
|
|
2681
|
+
refs.push(ref);
|
|
2682
|
+
}
|
|
2683
|
+
} catch {
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return { report, refs };
|
|
2687
|
+
}
|
|
2688
|
+
function isSubagentRollout(head) {
|
|
2689
|
+
const payload = firstRecord(head)?.["payload"];
|
|
2690
|
+
if (!isRecord(payload)) return false;
|
|
2691
|
+
const source = payload["source"];
|
|
2692
|
+
return isRecord(source) && source["subagent"] !== void 0;
|
|
2693
|
+
}
|
|
2694
|
+
function codexPlace(head) {
|
|
2695
|
+
if (isSubagentRollout(head)) return null;
|
|
2696
|
+
const record2 = firstRecord(head);
|
|
2697
|
+
if (!record2 || record2["type"] !== "session_meta" || !isRecord(record2["payload"])) return void 0;
|
|
2698
|
+
return readString(record2["payload"]["cwd"]);
|
|
2699
|
+
}
|
|
2700
|
+
function geminiProjectOf(candidate) {
|
|
2701
|
+
const local = nativePath();
|
|
2702
|
+
return local.basename(local.dirname(local.dirname(candidate.path)));
|
|
2703
|
+
}
|
|
2704
|
+
async function listGemini(options, limit, scope) {
|
|
2705
|
+
const store = geminiStore(options);
|
|
2706
|
+
const found = geminiStoreExists(store);
|
|
2707
|
+
const candidates = [];
|
|
2708
|
+
if (found) {
|
|
2709
|
+
for (const project of await subdirs(store.tmp)) {
|
|
2710
|
+
const chats = nativePath().join(project, "chats");
|
|
2711
|
+
candidates.push(...await statFiles(chats, (name) => /^session-.*\.jsonl?$/i.test(name)));
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const report = { agent: "gemini-cli", path: store.tmp, found, conversations: candidates.length };
|
|
2715
|
+
const refs = [];
|
|
2716
|
+
const folders = candidates.length > 0 ? geminiProjectFolders(store, [...options.cwds ?? [], ...scope?.folders ?? []]) : /* @__PURE__ */ new Map();
|
|
2717
|
+
const chosen = newest(
|
|
2718
|
+
scope ? candidates.filter((candidate) => {
|
|
2719
|
+
const cwd = folders.get(geminiProjectOf(candidate));
|
|
2720
|
+
return cwd !== void 0 && inScope(scope, cwd);
|
|
2721
|
+
}) : candidates,
|
|
2722
|
+
limit
|
|
2723
|
+
);
|
|
2724
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2725
|
+
for (const candidate of chosen) {
|
|
2726
|
+
try {
|
|
2727
|
+
const windows = await readWindows(candidate);
|
|
2728
|
+
const projectId = geminiProjectOf(candidate);
|
|
2729
|
+
const legacy = candidate.path.toLowerCase().endsWith(".json");
|
|
2730
|
+
const cwd = folders.get(projectId);
|
|
2731
|
+
const input = { path: candidate.path, bytes: candidate.bytes, mtime: new Date(candidate.mtimeMs).toISOString(), legacy, ...cwd ? { cwd } : {} };
|
|
2732
|
+
let ref;
|
|
2733
|
+
if (windows.whole) {
|
|
2734
|
+
const conversation = parseGeminiChat(windows.head, input);
|
|
2735
|
+
ref = refOf(conversation, conversation.turnCount);
|
|
2736
|
+
} else if (legacy) {
|
|
2737
|
+
continue;
|
|
2738
|
+
} else {
|
|
2739
|
+
ref = mergeWindows(parseGeminiChat(windows.head, input), parseGeminiChat(windows.tail, input), windows.tail.includes('"$set"'));
|
|
2740
|
+
}
|
|
2741
|
+
if (seen.has(ref.sessionId)) continue;
|
|
2742
|
+
seen.add(ref.sessionId);
|
|
2743
|
+
refs.push(ref);
|
|
2744
|
+
} catch {
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return { report, refs };
|
|
2748
|
+
}
|
|
2749
|
+
async function listOpencode(options, limit, scope) {
|
|
2750
|
+
const store = opencodeStore(options);
|
|
2751
|
+
const found = opencodeStoreExists(store);
|
|
2752
|
+
const report = { agent: "opencode", path: store.db, found, conversations: 0 };
|
|
2753
|
+
if (!found) return { report, refs: [] };
|
|
2754
|
+
const sqlite = await openSqlite(options.sqlite ? { sqlite: options.sqlite } : {});
|
|
2755
|
+
let db;
|
|
2756
|
+
if (sqlite) {
|
|
2757
|
+
try {
|
|
2758
|
+
db = sqlite.open(store.db);
|
|
2759
|
+
} catch {
|
|
2760
|
+
db = void 0;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (db) {
|
|
2764
|
+
try {
|
|
2765
|
+
return listOpencodeRows(db, store.db, report, limit, scope);
|
|
2766
|
+
} finally {
|
|
2767
|
+
db.close();
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
return listOpencodeStorage(store.storage, store.db, report, limit, scope);
|
|
2771
|
+
}
|
|
2772
|
+
function listOpencodeRows(db, dbPath, report, limit, scope) {
|
|
2773
|
+
const rows = db.all(
|
|
2774
|
+
"SELECT id, directory, title, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC"
|
|
2775
|
+
);
|
|
2776
|
+
report.conversations = rows.length;
|
|
2777
|
+
const refs = [];
|
|
2778
|
+
const chosen = scope ? rows.filter((row) => inScope(scope, readString(row["directory"]) ?? "")) : rows;
|
|
2779
|
+
for (const row of chosen.slice(0, limit)) {
|
|
2780
|
+
const id = readString(row["id"]);
|
|
2781
|
+
if (!id) continue;
|
|
2782
|
+
const bytes = (readNumber(db.all("SELECT COALESCE(SUM(LENGTH(data)), 0) AS b FROM message WHERE session_id = ?", id)[0]?.["b"]) ?? 0) + (readNumber(db.all("SELECT COALESCE(SUM(LENGTH(data)), 0) AS b FROM part WHERE session_id = ?", id)[0]?.["b"]) ?? 0);
|
|
2783
|
+
const compacted = db.all(`SELECT 1 AS one FROM part WHERE session_id = ? AND data LIKE '%"type":"compaction"%' LIMIT 1`, id).length > 0;
|
|
2784
|
+
const last = db.all("SELECT data, time_created FROM message WHERE session_id = ? ORDER BY time_created DESC, id DESC LIMIT 1", id)[0];
|
|
2785
|
+
const ref = opencodeRef(dbPath, id, row, null, bytes, compacted);
|
|
2786
|
+
const limitHit = last ? opencodeLimit(last) : void 0;
|
|
2787
|
+
if (limitHit) ref.limit = limitHit;
|
|
2788
|
+
refs.push(ref);
|
|
2789
|
+
}
|
|
2790
|
+
return { report, refs };
|
|
2791
|
+
}
|
|
2792
|
+
function opencodeRef(path, id, row, turnCount, bytes, compacted) {
|
|
2793
|
+
const ref = {
|
|
2794
|
+
id: conversationId("opencode", id),
|
|
2795
|
+
agent: "opencode",
|
|
2796
|
+
sessionId: id,
|
|
2797
|
+
handle: shortHandle("opencode", id),
|
|
2798
|
+
path,
|
|
2799
|
+
cwd: readString(row["directory"]) ?? "",
|
|
2800
|
+
updatedAt: isoFromEpochMs(row["time_updated"]) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2801
|
+
turnCount,
|
|
2802
|
+
bytes,
|
|
2803
|
+
compacted
|
|
2804
|
+
};
|
|
2805
|
+
const title = readString(row["title"]);
|
|
2806
|
+
if (title) ref.title = title;
|
|
2807
|
+
const startedAt = isoFromEpochMs(row["time_created"]);
|
|
2808
|
+
if (startedAt) ref.startedAt = startedAt;
|
|
2809
|
+
return ref;
|
|
2810
|
+
}
|
|
2811
|
+
function opencodeLimit(last) {
|
|
2812
|
+
let data = last["data"];
|
|
2813
|
+
if (typeof data === "string") {
|
|
2814
|
+
try {
|
|
2815
|
+
data = JSON.parse(data);
|
|
2816
|
+
} catch {
|
|
2817
|
+
return void 0;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (!isRecord(data) || data["role"] !== "assistant" || !isRecord(data["error"])) return void 0;
|
|
2821
|
+
const inner = isRecord(data["error"]["data"]) ? data["error"]["data"] : {};
|
|
2822
|
+
const status = readNumber(inner["statusCode"]);
|
|
2823
|
+
if (status !== 429 && status !== 402) return void 0;
|
|
2824
|
+
const time = isRecord(data["time"]) ? data["time"] : {};
|
|
2825
|
+
return { at: isoFromEpochMs(time["created"]) ?? isoFromEpochMs(last["time_created"]) ?? "", kind: status === 429 ? "rate-limit" : "credits" };
|
|
2826
|
+
}
|
|
2827
|
+
function opencodeStoragePlace(head) {
|
|
2828
|
+
let info;
|
|
2829
|
+
try {
|
|
2830
|
+
info = JSON.parse(head);
|
|
2831
|
+
} catch {
|
|
2832
|
+
return void 0;
|
|
2833
|
+
}
|
|
2834
|
+
if (!isRecord(info)) return void 0;
|
|
2835
|
+
if (readString(info["parentID"])) return null;
|
|
2836
|
+
return readString(info["directory"]);
|
|
2837
|
+
}
|
|
2838
|
+
async function listOpencodeStorage(storage, shownPath, report, limit, scope) {
|
|
2839
|
+
const local = nativePath();
|
|
2840
|
+
const candidates = [];
|
|
2841
|
+
for (const project of await subdirs(local.join(storage, "session"))) {
|
|
2842
|
+
candidates.push(...await statFiles(project, (name) => name.startsWith("ses_") && name.endsWith(".json")));
|
|
2843
|
+
}
|
|
2844
|
+
report.conversations = candidates.length;
|
|
2845
|
+
const refs = [];
|
|
2846
|
+
const chosen = newest(scope ? await withinScope(candidates, scope, opencodeStoragePlace) : candidates, limit);
|
|
2847
|
+
for (const candidate of chosen) {
|
|
2848
|
+
try {
|
|
2849
|
+
const windows = await readWindows(candidate);
|
|
2850
|
+
if (!windows.whole) continue;
|
|
2851
|
+
const info = JSON.parse(windows.head);
|
|
2852
|
+
if (!isRecord(info)) continue;
|
|
2853
|
+
if (readString(info["parentID"])) continue;
|
|
2854
|
+
const id = readString(info["id"]) ?? local.basename(candidate.path, ".json");
|
|
2855
|
+
const time = isRecord(info["time"]) ? info["time"] : {};
|
|
2856
|
+
const messages = await statFiles(local.join(storage, "message", id), (name) => name.endsWith(".json"));
|
|
2857
|
+
const row = { directory: info["directory"], title: info["title"], time_created: time["created"], time_updated: time["updated"] };
|
|
2858
|
+
refs.push(opencodeRef(shownPath, id, row, null, messages.reduce((n, m) => n + m.bytes, 0), false));
|
|
2859
|
+
} catch {
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return { report, refs };
|
|
2863
|
+
}
|
|
2864
|
+
var MIN_PREFIX = 4;
|
|
2865
|
+
function resolveConversation(handleOrPrefix, discovery) {
|
|
2866
|
+
const wanted = handleOrPrefix.trim();
|
|
2867
|
+
const split = splitConversationId(wanted);
|
|
2868
|
+
if (split && isAgentId(split.agent)) {
|
|
2869
|
+
const exact = discovery.conversations.find((ref) => ref.id === wanted);
|
|
2870
|
+
if (exact) return exact;
|
|
2871
|
+
throw new HandoffFault("conversation-not-found", wanted);
|
|
2872
|
+
}
|
|
2873
|
+
if (!isSafeId(wanted)) throw new HandoffFault("invalid-id", wanted);
|
|
2874
|
+
if (wanted.length < MIN_PREFIX) throw new HandoffFault("invalid-id", `at least ${MIN_PREFIX} characters`);
|
|
2875
|
+
const bySession = discovery.conversations.filter((ref) => ref.sessionId === wanted);
|
|
2876
|
+
if (bySession.length === 1) return bySession[0];
|
|
2877
|
+
const lower = wanted.toLowerCase();
|
|
2878
|
+
const matches = discovery.conversations.filter((ref) => {
|
|
2879
|
+
const handle = shortHandle(ref.agent, ref.sessionId).toLowerCase();
|
|
2880
|
+
const body = (ref.agent === "opencode" && ref.sessionId.startsWith("ses_") ? ref.sessionId.slice(4) : ref.sessionId).toLowerCase();
|
|
2881
|
+
return handle.startsWith(lower) || body.startsWith(lower) || ref.sessionId.toLowerCase().startsWith(lower);
|
|
2882
|
+
});
|
|
2883
|
+
if (matches.length === 1) return matches[0];
|
|
2884
|
+
if (matches.length === 0) throw new HandoffFault("conversation-not-found", wanted);
|
|
2885
|
+
throw new HandoffFault("ambiguous-id", matches.map((ref) => `${ref.agent}:${ref.handle}`).join(", "));
|
|
2886
|
+
}
|
|
2887
|
+
var SAME_HOUR_MS = 60 * 60 * 1e3;
|
|
2888
|
+
function newestOfFolder(refs) {
|
|
2889
|
+
const [newest2] = refs;
|
|
2890
|
+
if (!newest2) return { newest: void 0, rival: void 0 };
|
|
2891
|
+
const since = Date.parse(newest2.updatedAt);
|
|
2892
|
+
const rival = refs.slice(1).find((ref) => ref.agent !== newest2.agent && since - Date.parse(ref.updatedAt) < SAME_HOUR_MS);
|
|
2893
|
+
return { newest: newest2, rival };
|
|
2894
|
+
}
|
|
2895
|
+
function toBundle(conversation, digest, now = /* @__PURE__ */ new Date()) {
|
|
2896
|
+
return { format: "panoma-conversation", version: 1, exportedAt: now.toISOString(), conversation, digest };
|
|
2897
|
+
}
|
|
2898
|
+
function isBundlePath(value) {
|
|
2899
|
+
return value.includes("/") || value.includes("\\") || value.toLowerCase().endsWith(".json");
|
|
2900
|
+
}
|
|
2901
|
+
function fromBundle(value) {
|
|
2902
|
+
const record2 = asRecord(value, "not an object");
|
|
2903
|
+
if (record2["format"] !== "panoma-conversation") throw invalid("format");
|
|
2904
|
+
if (record2["version"] !== 1) throw invalid("version");
|
|
2905
|
+
const exportedAt = record2["exportedAt"];
|
|
2906
|
+
if (typeof exportedAt !== "string") throw invalid("exportedAt");
|
|
2907
|
+
const conversation = conversationOf(record2["conversation"]);
|
|
2908
|
+
const digest = digestOf(record2["digest"]);
|
|
2909
|
+
return { format: "panoma-conversation", version: 1, exportedAt, conversation, digest };
|
|
2910
|
+
}
|
|
2911
|
+
function invalid(detail) {
|
|
2912
|
+
return new HandoffFault("bundle-invalid", detail);
|
|
2913
|
+
}
|
|
2914
|
+
function asRecord(value, detail) {
|
|
2915
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalid(detail);
|
|
2916
|
+
return value;
|
|
2917
|
+
}
|
|
2918
|
+
function str(record2, key, where) {
|
|
2919
|
+
const value = record2[key];
|
|
2920
|
+
if (typeof value !== "string") throw invalid(`${where}.${key}`);
|
|
2921
|
+
return value;
|
|
2922
|
+
}
|
|
2923
|
+
function optionalStr(record2, key, where) {
|
|
2924
|
+
const value = record2[key];
|
|
2925
|
+
if (value === void 0 || value === null) return void 0;
|
|
2926
|
+
if (typeof value !== "string") throw invalid(`${where}.${key}`);
|
|
2927
|
+
return value;
|
|
2928
|
+
}
|
|
2929
|
+
function strings(value, where) {
|
|
2930
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw invalid(where);
|
|
2931
|
+
return value;
|
|
2932
|
+
}
|
|
2933
|
+
function conversationOf(value) {
|
|
2934
|
+
const c = asRecord(value, "conversation");
|
|
2935
|
+
const agent = str(c, "agent", "conversation");
|
|
2936
|
+
if (!isAgentId(agent)) throw invalid("conversation.agent");
|
|
2937
|
+
const turnsRaw = c["turns"];
|
|
2938
|
+
if (!Array.isArray(turnsRaw)) throw invalid("conversation.turns");
|
|
2939
|
+
const turns = turnsRaw.map((turn, index) => turnOf(turn, `conversation.turns[${index}]`));
|
|
2940
|
+
const turnCount = c["turnCount"];
|
|
2941
|
+
if (turnCount !== null && typeof turnCount !== "number") throw invalid("conversation.turnCount");
|
|
2942
|
+
const bytes = c["bytes"];
|
|
2943
|
+
if (typeof bytes !== "number") throw invalid("conversation.bytes");
|
|
2944
|
+
const compactionsRaw = c["compactions"];
|
|
2945
|
+
if (!Array.isArray(compactionsRaw)) throw invalid("conversation.compactions");
|
|
2946
|
+
const compactions = compactionsRaw.map((item, index) => {
|
|
2947
|
+
const r = asRecord(item, `conversation.compactions[${index}]`);
|
|
2948
|
+
const out = { text: str(r, "text", `conversation.compactions[${index}]`) };
|
|
2949
|
+
const at = optionalStr(r, "at", `conversation.compactions[${index}]`);
|
|
2950
|
+
if (at) out.at = at;
|
|
2951
|
+
if (typeof r["tokensBefore"] === "number") out.tokensBefore = r["tokensBefore"];
|
|
2952
|
+
return out;
|
|
2953
|
+
});
|
|
2954
|
+
const conversation = {
|
|
2955
|
+
version: 1,
|
|
2956
|
+
id: str(c, "id", "conversation"),
|
|
2957
|
+
agent,
|
|
2958
|
+
sessionId: str(c, "sessionId", "conversation"),
|
|
2959
|
+
handle: str(c, "handle", "conversation"),
|
|
2960
|
+
path: str(c, "path", "conversation"),
|
|
2961
|
+
cwd: str(c, "cwd", "conversation"),
|
|
2962
|
+
updatedAt: str(c, "updatedAt", "conversation"),
|
|
2963
|
+
turnCount,
|
|
2964
|
+
bytes,
|
|
2965
|
+
compacted: c["compacted"] === true,
|
|
2966
|
+
hash: typeof c["hash"] === "string" ? c["hash"] : hashTurns(turns),
|
|
2967
|
+
turns,
|
|
2968
|
+
compactions,
|
|
2969
|
+
dropped: droppedOf(c["dropped"])
|
|
2970
|
+
};
|
|
2971
|
+
for (const key of ["gitBranch", "title", "model", "startedAt"]) {
|
|
2972
|
+
const v = optionalStr(c, key, "conversation");
|
|
2973
|
+
if (v) conversation[key] = v;
|
|
2974
|
+
}
|
|
2975
|
+
const limit = c["limit"];
|
|
2976
|
+
if (limit !== void 0 && limit !== null) {
|
|
2977
|
+
const l = asRecord(limit, "conversation.limit");
|
|
2978
|
+
conversation.limit = { at: str(l, "at", "conversation.limit") };
|
|
2979
|
+
const resetsAt = optionalStr(l, "resetsAt", "conversation.limit");
|
|
2980
|
+
if (resetsAt) conversation.limit.resetsAt = resetsAt;
|
|
2981
|
+
const kind = optionalStr(l, "kind", "conversation.limit");
|
|
2982
|
+
if (kind) conversation.limit.kind = kind;
|
|
2983
|
+
}
|
|
2984
|
+
return conversation;
|
|
2985
|
+
}
|
|
2986
|
+
function turnOf(value, where) {
|
|
2987
|
+
const t = asRecord(value, where);
|
|
2988
|
+
const role = t["role"];
|
|
2989
|
+
if (role !== "user" && role !== "assistant") throw invalid(`${where}.role`);
|
|
2990
|
+
const partsRaw = t["parts"];
|
|
2991
|
+
if (!Array.isArray(partsRaw)) throw invalid(`${where}.parts`);
|
|
2992
|
+
const turn = { role, parts: partsRaw.map((part, index) => partOf(part, `${where}.parts[${index}]`)) };
|
|
2993
|
+
const at = optionalStr(t, "at", where);
|
|
2994
|
+
if (at) turn.at = at;
|
|
2995
|
+
return turn;
|
|
2996
|
+
}
|
|
2997
|
+
function partOf(value, where) {
|
|
2998
|
+
const p = asRecord(value, where);
|
|
2999
|
+
switch (p["kind"]) {
|
|
3000
|
+
case "text":
|
|
3001
|
+
case "summary":
|
|
3002
|
+
return { kind: p["kind"], text: str(p, "text", where) };
|
|
3003
|
+
case "tool_call":
|
|
3004
|
+
return { kind: "tool_call", id: str(p, "id", where), name: str(p, "name", where), input: p["input"] };
|
|
3005
|
+
case "tool_result": {
|
|
3006
|
+
const part = { kind: "tool_result", callId: str(p, "callId", where), output: str(p, "output", where) };
|
|
3007
|
+
if (p["isError"] === true) part.isError = true;
|
|
3008
|
+
return part;
|
|
3009
|
+
}
|
|
3010
|
+
default:
|
|
3011
|
+
throw invalid(`${where}.kind`);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
function droppedOf(value) {
|
|
3015
|
+
const d = asRecord(value, "conversation.dropped");
|
|
3016
|
+
const out = { thinking: 0, images: 0, subagents: 0, offloaded: 0, secrets: 0, other: 0 };
|
|
3017
|
+
for (const key of Object.keys(out)) {
|
|
3018
|
+
const n = d[key];
|
|
3019
|
+
if (n !== void 0 && typeof n !== "number") throw invalid(`conversation.dropped.${key}`);
|
|
3020
|
+
out[key] = typeof n === "number" ? n : 0;
|
|
3021
|
+
}
|
|
3022
|
+
return out;
|
|
3023
|
+
}
|
|
3024
|
+
function digestOf(value) {
|
|
3025
|
+
const d = asRecord(value, "digest");
|
|
3026
|
+
const by = d["by"];
|
|
3027
|
+
if (by !== "panoma" && by !== "model") throw invalid("digest.by");
|
|
3028
|
+
const exchange = asRecord(d["lastExchange"] ?? {}, "digest.lastExchange");
|
|
3029
|
+
const stats = asRecord(d["stats"] ?? {}, "digest.stats");
|
|
3030
|
+
const digest = {
|
|
3031
|
+
by,
|
|
3032
|
+
title: str(d, "title", "digest"),
|
|
3033
|
+
goal: str(d, "goal", "digest"),
|
|
3034
|
+
decisions: strings(d["decisions"], "digest.decisions"),
|
|
3035
|
+
filesTouched: strings(d["filesTouched"], "digest.filesTouched"),
|
|
3036
|
+
commandsRun: strings(d["commandsRun"], "digest.commandsRun"),
|
|
3037
|
+
openItems: strings(d["openItems"], "digest.openItems"),
|
|
3038
|
+
lastExchange: {},
|
|
3039
|
+
stats: {
|
|
3040
|
+
turns: typeof stats["turns"] === "number" ? stats["turns"] : 0,
|
|
3041
|
+
toolCalls: typeof stats["toolCalls"] === "number" ? stats["toolCalls"] : 0,
|
|
3042
|
+
estimatedTokens: typeof stats["estimatedTokens"] === "number" ? stats["estimatedTokens"] : 0
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
const summary = optionalStr(d, "summary", "digest");
|
|
3046
|
+
if (summary) digest.summary = summary;
|
|
3047
|
+
const user = optionalStr(exchange, "user", "digest.lastExchange");
|
|
3048
|
+
if (user) digest.lastExchange.user = user;
|
|
3049
|
+
const assistant = optionalStr(exchange, "assistant", "digest.lastExchange");
|
|
3050
|
+
if (assistant) digest.lastExchange.assistant = assistant;
|
|
3051
|
+
return digest;
|
|
3052
|
+
}
|
|
3053
|
+
async function handoff(input) {
|
|
3054
|
+
const plan = await prepare(input);
|
|
3055
|
+
if (plan.kind === "document") return writeDocument(input, plan.digest, plan.now, plan.keepTurns);
|
|
3056
|
+
switch (plan.target) {
|
|
3057
|
+
case "claude-cli":
|
|
3058
|
+
return writeClaudeConversation(plan.request);
|
|
3059
|
+
case "codex-cli":
|
|
3060
|
+
return writeCodexConversation(plan.request);
|
|
3061
|
+
case "opencode":
|
|
3062
|
+
return writeOpencodeConversation(plan.request);
|
|
3063
|
+
case "gemini-cli":
|
|
3064
|
+
return writeGeminiConversation(plan.request);
|
|
3065
|
+
default:
|
|
3066
|
+
throw new HandoffFault("unsupported-target", plan.target);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
async function prepare(input) {
|
|
3070
|
+
const options = input.options ?? {};
|
|
3071
|
+
const resolved = resolveStoreOptions(options);
|
|
3072
|
+
const { conversation, target, tier } = input;
|
|
3073
|
+
if (!isAgentId(target)) throw new HandoffFault("unsupported-target", String(target));
|
|
3074
|
+
if (conversation.bytes > MAX_CONVERSATION_BYTES) throw new HandoffFault("too-large", String(conversation.bytes));
|
|
3075
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
3076
|
+
const random = input.random ?? cryptoRandom;
|
|
3077
|
+
const keepTurns = Math.max(1, Math.floor(input.keepTurns ?? KEEP_TURNS_DEFAULT));
|
|
3078
|
+
const digest = input.digest ?? digestConversation(conversation);
|
|
3079
|
+
if (!isNativeTarget(target) || tier === "brief") {
|
|
3080
|
+
return { kind: "document", digest, now, keepTurns };
|
|
3081
|
+
}
|
|
3082
|
+
if (target === conversation.agent && tier === "full" && !input.targetHome) throw new HandoffFault("same-store", target);
|
|
3083
|
+
const root = await resolveRoot(target, input.targetHome, options, conversation, tier);
|
|
3084
|
+
const cwd = input.cwd ?? conversation.cwd;
|
|
3085
|
+
if (!cwd) throw new HandoffFault("cwd-missing");
|
|
3086
|
+
const folder = await stat7(cwd).catch(() => void 0);
|
|
3087
|
+
if (!folder?.isDirectory()) throw new HandoffFault("cwd-missing", cwd);
|
|
3088
|
+
const tiered = tier === "compact" ? compactConversation(conversation, digest, { keepTurns }) : conversation;
|
|
3089
|
+
if (!carriesAnything(tiered)) throw new HandoffFault("nothing-to-carry");
|
|
3090
|
+
const request = {
|
|
3091
|
+
conversation: tiered,
|
|
3092
|
+
tier,
|
|
3093
|
+
root,
|
|
3094
|
+
cwd,
|
|
3095
|
+
title: digest.title,
|
|
3096
|
+
now,
|
|
3097
|
+
random,
|
|
3098
|
+
platform: resolved.platform,
|
|
3099
|
+
surface: input.surface ?? "cli"
|
|
3100
|
+
};
|
|
3101
|
+
const branch = input.gitBranch ?? conversation.gitBranch;
|
|
3102
|
+
if (branch) request.gitBranch = branch;
|
|
3103
|
+
return { kind: "native", target, request };
|
|
3104
|
+
}
|
|
3105
|
+
function carriesAnything(conversation) {
|
|
3106
|
+
return conversation.turns.some(
|
|
3107
|
+
(turn) => turn.parts.some((part) => part.kind === "tool_call" ? true : part.kind === "tool_result" ? part.output.length > 0 : part.text.trim().length > 0)
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
async function resolveRoot(target, targetHome, options, conversation, tier) {
|
|
3111
|
+
const resolved = resolveStoreOptions(options);
|
|
3112
|
+
if (targetHome !== void 0) {
|
|
3113
|
+
if (!isAbsoluteOn(targetHome, resolved.platform)) throw new HandoffFault("target-store-missing", "must be absolute");
|
|
3114
|
+
const store = storeAt(target, targetHome, options);
|
|
3115
|
+
if (!store.exists) throw new HandoffFault("target-store-missing", targetHome);
|
|
3116
|
+
if (target === conversation.agent && tier === "full") {
|
|
3117
|
+
const own = defaultRoot(target, options);
|
|
3118
|
+
const [a, b] = await Promise.all([realFolder(targetHome), realFolder(own)]);
|
|
3119
|
+
if (sameFolder(a, b, resolved.platform)) throw new HandoffFault("same-store", targetHome);
|
|
3120
|
+
}
|
|
3121
|
+
return targetHome;
|
|
3122
|
+
}
|
|
3123
|
+
const root = defaultRoot(target, options);
|
|
3124
|
+
if (!exists(root)) throw new HandoffFault("target-store-missing", root);
|
|
3125
|
+
return root;
|
|
3126
|
+
}
|
|
3127
|
+
function defaultRoot(target, options) {
|
|
3128
|
+
switch (target) {
|
|
3129
|
+
case "claude-cli":
|
|
3130
|
+
return claudeStore(options).root;
|
|
3131
|
+
case "codex-cli":
|
|
3132
|
+
return codexStore(options).root;
|
|
3133
|
+
case "opencode":
|
|
3134
|
+
return opencodeStore(options).root;
|
|
3135
|
+
case "gemini-cli":
|
|
3136
|
+
return geminiStore(options).root;
|
|
3137
|
+
default:
|
|
3138
|
+
throw new HandoffFault("unsupported-target", target);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
function storeAt(target, root, options) {
|
|
3142
|
+
switch (target) {
|
|
3143
|
+
case "claude-cli":
|
|
3144
|
+
return { exists: claudeStoreExists(claudeStoreAt(root, options)) };
|
|
3145
|
+
case "codex-cli":
|
|
3146
|
+
return { exists: codexStoreExists(codexStoreAt(root, options)) };
|
|
3147
|
+
case "opencode":
|
|
3148
|
+
return { exists: opencodeStoreExists(opencodeStoreAt(root, options)) };
|
|
3149
|
+
case "gemini-cli":
|
|
3150
|
+
return { exists: geminiStoreExists(geminiStoreAt(root, options)) };
|
|
3151
|
+
default:
|
|
3152
|
+
throw new HandoffFault("unsupported-target", target);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async function writeDocument(input, digest, now, keepTurns) {
|
|
3156
|
+
const { conversation, target } = input;
|
|
3157
|
+
const redactor = new Redactor();
|
|
3158
|
+
const markdown = briefMarkdown(conversation, digest, { keepTurns, now, redactor });
|
|
3159
|
+
const stem = `${conversation.agent}-${conversation.handle}-${now.toISOString().slice(0, 10)}`;
|
|
3160
|
+
const path = input.out ?? panomaPath("handoff", `${stem}.md`);
|
|
3161
|
+
const bytes = await writeBrief(path, markdown);
|
|
3162
|
+
return {
|
|
3163
|
+
agent: target,
|
|
3164
|
+
surface: input.surface ?? "cli",
|
|
3165
|
+
sessionId: stem,
|
|
3166
|
+
path,
|
|
3167
|
+
resume: void 0,
|
|
3168
|
+
resumeInApp: void 0,
|
|
3169
|
+
steps: [],
|
|
3170
|
+
fidelity: fidelityOf(target),
|
|
3171
|
+
provenance: {
|
|
3172
|
+
sourceAgent: conversation.agent,
|
|
3173
|
+
sourceSessionId: conversation.sessionId,
|
|
3174
|
+
sourceHash: conversation.hash,
|
|
3175
|
+
tier: "brief",
|
|
3176
|
+
at: now.toISOString(),
|
|
3177
|
+
by: "panoma"
|
|
3178
|
+
},
|
|
3179
|
+
turns: Math.min(keepTurns, conversation.turns.length),
|
|
3180
|
+
bytes,
|
|
3181
|
+
dropped: { ...conversation.dropped, secrets: conversation.dropped.secrets + redactor.count }
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
// src/handoff-command.ts
|
|
3186
|
+
var run = promisify(execFile);
|
|
3187
|
+
var TARGET_WORDS = [...Object.keys(AGENT_WORDS), ...Object.keys(APP_WORDS), "bundle"];
|
|
3188
|
+
function labelOf(ref) {
|
|
3189
|
+
return (ref.surface === "app" ? APP_OF[ref.agent]?.name : void 0) ?? AGENT_NAMES[ref.agent];
|
|
3190
|
+
}
|
|
3191
|
+
async function handoffCommand(parsed, deps = {}) {
|
|
3192
|
+
const [, source, extra] = parsed.positionals;
|
|
3193
|
+
if (extra !== void 0) {
|
|
3194
|
+
process.stderr.write(
|
|
3195
|
+
pc.red(`${say("handoff.extraArgument", { extra })}
|
|
3196
|
+
`) + pc.dim(`${say("handoff.usage")}
|
|
3197
|
+
${say("handoff.usageHint")}
|
|
3198
|
+
`)
|
|
3199
|
+
);
|
|
3200
|
+
return 1;
|
|
3201
|
+
}
|
|
3202
|
+
const cwd = await realFolder(deps.cwd ?? process.cwd());
|
|
3203
|
+
const store = deps.store ?? {};
|
|
3204
|
+
const tier = parsed.tier ?? "full";
|
|
3205
|
+
const digestBy = parsed.digest ?? "panoma";
|
|
3206
|
+
const out = parsed.out ? resolve2(expandTilde(parsed.out)) : void 0;
|
|
3207
|
+
let target;
|
|
3208
|
+
let surface = "cli";
|
|
3209
|
+
if (parsed.to !== void 0) {
|
|
3210
|
+
const word = parsed.to.trim().toLowerCase();
|
|
3211
|
+
const app = agentOfApp(word);
|
|
3212
|
+
if (word === "bundle") target = "bundle";
|
|
3213
|
+
else if (app !== void 0) {
|
|
3214
|
+
target = app;
|
|
3215
|
+
surface = "app";
|
|
3216
|
+
} else target = agentFromWord(word);
|
|
3217
|
+
if (target === void 0) {
|
|
3218
|
+
const guess = nearestWord(word);
|
|
3219
|
+
process.stderr.write(
|
|
3220
|
+
pc.red(`${say("handoff.unknownTarget", { target: parsed.to })}${guess ? ` ${say("error.didYouMean", { guess })}` : ""}
|
|
3221
|
+
`) + pc.dim(`${say("handoff.targets", { list: TARGET_WORDS.join(" \xB7 ") })}
|
|
3222
|
+
`)
|
|
3223
|
+
);
|
|
3224
|
+
return 1;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
if (source === void 0 && target === void 0 && tier !== "brief") {
|
|
3228
|
+
return list(parsed, cwd, store);
|
|
3229
|
+
}
|
|
3230
|
+
const loaded = await load(source, target === void 0 || target === "bundle" ? "<agent>" : wordOf(target, surface), parsed, cwd, store);
|
|
3231
|
+
if (typeof loaded === "number") return loaded;
|
|
3232
|
+
const { conversation, taken } = loaded;
|
|
3233
|
+
let digest = loaded.digest;
|
|
3234
|
+
if (digestBy === "model" && !parsed.dryRun) {
|
|
3235
|
+
const written = await modelDigest(parsed, conversation);
|
|
3236
|
+
if (typeof written === "number") return written;
|
|
3237
|
+
digest = written;
|
|
3238
|
+
} else {
|
|
3239
|
+
if (digestBy === "model" && !parsed.json) process.stderr.write(pc.dim(`${say("handoff.dryRunDigest")}
|
|
3240
|
+
`));
|
|
3241
|
+
digest ??= digestConversation(conversation);
|
|
3242
|
+
}
|
|
3243
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
3244
|
+
if (target === "bundle") {
|
|
3245
|
+
if (parsed.dryRun) return preview(parsed, conversation, digest, "bundle", "full", out);
|
|
3246
|
+
const covered = coverBundle(conversation, digest);
|
|
3247
|
+
const bundle = toBundle(covered.conversation, covered.digest, now);
|
|
3248
|
+
const json = `${JSON.stringify(bundle, null, 2)}
|
|
3249
|
+
`;
|
|
3250
|
+
if (out) {
|
|
3251
|
+
await writeFile3(out, json, { encoding: "utf8", mode: 384 });
|
|
3252
|
+
if (parsed.json) process.stdout.write(`${JSON.stringify({ ok: true, bundle: out, dropped: covered.conversation.dropped }, null, 2)}
|
|
3253
|
+
`);
|
|
3254
|
+
else process.stderr.write(pc.green(`\u2713 ${say("handoff.bundleWritten", { path: out })}
|
|
3255
|
+
`));
|
|
3256
|
+
return 0;
|
|
3257
|
+
}
|
|
3258
|
+
process.stdout.write(json);
|
|
3259
|
+
return 0;
|
|
3260
|
+
}
|
|
3261
|
+
const documentOnly2 = target !== void 0 && !isNativeTarget(target);
|
|
3262
|
+
if (tier === "brief" || documentOnly2) {
|
|
3263
|
+
if (documentOnly2 && !parsed.json) {
|
|
3264
|
+
process.stderr.write(pc.dim(`${say("handoff.documentOnly", { agent: AGENT_NAMES[target] })}
|
|
3265
|
+
`));
|
|
3266
|
+
}
|
|
3267
|
+
if (parsed.dryRun) return preview(parsed, conversation, digest, target, "brief");
|
|
3268
|
+
const options = { keepTurns: parsed.keep, now };
|
|
3269
|
+
if (out === void 0) {
|
|
3270
|
+
const markdown = briefMarkdown(conversation, digest, options);
|
|
3271
|
+
if (parsed.json) process.stdout.write(`${JSON.stringify({ ok: true, tier: "brief", document: markdown }, null, 2)}
|
|
3272
|
+
`);
|
|
3273
|
+
else process.stdout.write(markdown.endsWith("\n") ? markdown : `${markdown}
|
|
3274
|
+
`);
|
|
3275
|
+
return 0;
|
|
3276
|
+
}
|
|
3277
|
+
return write({ parsed, deps, conversation, digest, target: target ?? conversation.agent, surface, named: target !== void 0, tier: "brief", out, taken, cwd, store, now });
|
|
3278
|
+
}
|
|
3279
|
+
if (target === void 0) {
|
|
3280
|
+
if (!parsed.json) process.stderr.write(pc.dim(`${say("handoff.hint", { handle: conversation.handle })}
|
|
3281
|
+
`));
|
|
3282
|
+
return preview(parsed, conversation, digest, void 0, tier);
|
|
3283
|
+
}
|
|
3284
|
+
if (target === conversation.agent && parsed.targetHome === void 0 && tier === "full") {
|
|
3285
|
+
if (parsed.dryRun && !parsed.json) process.stderr.write(pc.dim(`${say("handoff.dryRunSameAgent")}
|
|
3286
|
+
`));
|
|
3287
|
+
return sameAgent(parsed, conversation, cwd, store.platform);
|
|
3288
|
+
}
|
|
3289
|
+
if (parsed.dryRun) return preview(parsed, conversation, digest, target, tier);
|
|
3290
|
+
return write({ parsed, deps, conversation, digest, target, surface, named: true, tier, out, taken, cwd, store, now });
|
|
3291
|
+
}
|
|
3292
|
+
async function list(parsed, cwd, store) {
|
|
3293
|
+
const projects = await catalogProjects(parsed.api);
|
|
3294
|
+
const roots = projects === void 0 ? [] : projects.map((project) => project.root);
|
|
3295
|
+
const discovery = await discover(store, { cwds: [cwd, ...roots] });
|
|
3296
|
+
if (typeof discovery === "number") return discovery;
|
|
3297
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3298
|
+
if (parsed.json) {
|
|
3299
|
+
process.stdout.write(`${JSON.stringify({ conversations: discovery.conversations, stores: discovery.stores, catalog: projects !== void 0 }, null, 2)}
|
|
3300
|
+
`);
|
|
3301
|
+
return 0;
|
|
3302
|
+
}
|
|
3303
|
+
if (discovery.conversations.length === 0) {
|
|
3304
|
+
process.stdout.write(["", ` ${pc.yellow(say("handoff.emptyAll"))}`, ...storeLines(discovery.stores), "", ` ${pc.dim(say("handoff.storesHint"))}`, ""].join("\n"));
|
|
3305
|
+
return 0;
|
|
3306
|
+
}
|
|
3307
|
+
const lines = [""];
|
|
3308
|
+
const here = await inFolder(discovery.conversations, cwd, store.platform);
|
|
3309
|
+
const rest = discovery.conversations.filter((ref) => !here.includes(ref));
|
|
3310
|
+
if (here.length > 0) {
|
|
3311
|
+
lines.push(` ${pc.bold(say("handoff.inFolder"))}`, ...rowLines(here, now), "");
|
|
3312
|
+
}
|
|
3313
|
+
if (projects === void 0) {
|
|
3314
|
+
if (rest.length > 0) lines.push(` ${pc.bold(say("handoff.elsewhere"))}`, ...rowLines(rest, now), "");
|
|
3315
|
+
lines.push(` ${pc.dim(say("handoff.catalogDown"))}`, "");
|
|
3316
|
+
} else {
|
|
3317
|
+
for (const group of groupByProject(rest, projects, store.platform)) {
|
|
3318
|
+
lines.push(` ${pc.bold(group.slug ?? say("handoff.notInCatalog"))}`, ...rowLines(group.conversations, now), "");
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
process.stdout.write(lines.join("\n"));
|
|
3322
|
+
return 0;
|
|
3323
|
+
}
|
|
3324
|
+
async function catalogProjects(api) {
|
|
3325
|
+
try {
|
|
3326
|
+
const reply = await catalogFetch(new URL("/api/catalog", api));
|
|
3327
|
+
if (!reply.ok) return void 0;
|
|
3328
|
+
const body = await reply.json();
|
|
3329
|
+
return (body.projects ?? []).filter((project) => typeof project.root === "string" && typeof project.slug === "string");
|
|
3330
|
+
} catch {
|
|
3331
|
+
return void 0;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
function groupByProject(conversations, projects, platform) {
|
|
3335
|
+
const deepestFirst = [...projects].sort((a, b) => b.root.length - a.root.length);
|
|
3336
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3337
|
+
for (const ref of conversations) {
|
|
3338
|
+
const owner = deepestFirst.find((project) => insideFolder(ref.cwd, project.root, platform));
|
|
3339
|
+
const key = owner?.slug;
|
|
3340
|
+
const bucket = groups.get(key);
|
|
3341
|
+
if (bucket) bucket.push(ref);
|
|
3342
|
+
else groups.set(key, [ref]);
|
|
3343
|
+
}
|
|
3344
|
+
const named = [...groups.entries()].filter(([slug]) => slug !== void 0).sort(([a], [b]) => a.localeCompare(b));
|
|
3345
|
+
const orphans = groups.get(void 0);
|
|
3346
|
+
return [
|
|
3347
|
+
...named.map(([slug, refs]) => ({ slug, conversations: refs })),
|
|
3348
|
+
...orphans ? [{ slug: void 0, conversations: orphans }] : []
|
|
3349
|
+
];
|
|
3350
|
+
}
|
|
3351
|
+
function rowLines(refs, now) {
|
|
3352
|
+
const lines = [];
|
|
3353
|
+
for (const ref of refs) {
|
|
3354
|
+
const when = sinceMs(ref.updatedAt, now) || ref.updatedAt.slice(0, 10);
|
|
3355
|
+
const facts = ref.turnCount === null ? say("handoff.rowSize", { when, size: size(ref.bytes) }) : say("handoff.rowTurns", { when, n: ref.turnCount });
|
|
3356
|
+
const tags = [ref.compacted ? say("handoff.rowSummary") : "", limitWord(ref, now)].filter(Boolean);
|
|
3357
|
+
lines.push(
|
|
3358
|
+
` ${pc.cyan(ref.handle.padEnd(10))}${pc.dim(labelOf(ref).padEnd(16))}${ref.title ?? pc.dim(ref.cwd)}`,
|
|
3359
|
+
` ${pc.dim([facts, ...tags].join(" \xB7 "))}`
|
|
3360
|
+
);
|
|
3361
|
+
if (ref.limit) {
|
|
3362
|
+
const target = ref.agent === "codex-cli" ? "claude" : "codex";
|
|
3363
|
+
lines.push(` ${say("handoff.readyLine", { handle: ref.handle, target })}`);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return lines;
|
|
3367
|
+
}
|
|
3368
|
+
function limitWord(ref, now) {
|
|
3369
|
+
if (!ref.limit) return "";
|
|
3370
|
+
if (!ref.limit.resetsAt) return say("handoff.rowLimit");
|
|
3371
|
+
const reset = Date.parse(ref.limit.resetsAt);
|
|
3372
|
+
if (!Number.isFinite(reset)) return say("handoff.rowLimit");
|
|
3373
|
+
if (reset > Date.parse(now)) {
|
|
3374
|
+
const minutes = Math.max(1, Math.round((reset - Date.parse(now)) / 6e4));
|
|
3375
|
+
const when = minutes < 60 ? say("handoff.inMinutes", { n: minutes }) : say("handoff.inHours", { n: Math.round(minutes / 60) });
|
|
3376
|
+
return say("handoff.rowLimitBack", { when });
|
|
3377
|
+
}
|
|
3378
|
+
return say("handoff.rowLimitLifted", { when: sinceMs(ref.limit.resetsAt, now) });
|
|
3379
|
+
}
|
|
3380
|
+
function storeLines(stores) {
|
|
3381
|
+
return stores.map(
|
|
3382
|
+
(report) => ` ${pc.dim(
|
|
3383
|
+
report.found ? say("handoff.storeFound", { agent: AGENT_NAMES[report.agent], path: report.path, n: report.conversations }) : say("handoff.storeMissing", { agent: AGENT_NAMES[report.agent], path: report.path })
|
|
3384
|
+
)}`
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
async function load(source, targetWord, parsed, cwd, store) {
|
|
3388
|
+
if (source !== void 0 && isBundlePath(source)) {
|
|
3389
|
+
try {
|
|
3390
|
+
const text = await readFile6(resolve2(expandTilde(source)), "utf8");
|
|
3391
|
+
const bundle = fromBundle(JSON.parse(text));
|
|
3392
|
+
return { conversation: bundle.conversation, digest: bundle.digest };
|
|
3393
|
+
} catch (error) {
|
|
3394
|
+
process.stderr.write(pc.red(`${handoffFaultText(error instanceof SyntaxError ? "bundle-invalid: not JSON" : error)}
|
|
3395
|
+
`));
|
|
3396
|
+
return 1;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
if (source === void 0) {
|
|
3400
|
+
const discovery2 = await discover(store, { cwds: [cwd] });
|
|
3401
|
+
if (typeof discovery2 === "number") return discovery2;
|
|
3402
|
+
const { newest: first, rival } = newestOfFolder(await inFolder(discovery2.conversations, cwd, store.platform));
|
|
3403
|
+
if (!first) {
|
|
3404
|
+
process.stderr.write(pc.red(`${say("handoff.noneHere", { cwd })}
|
|
3405
|
+
`) + pc.dim(`${say("handoff.noneHereHint")}
|
|
3406
|
+
`));
|
|
3407
|
+
return 1;
|
|
3408
|
+
}
|
|
3409
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3410
|
+
if (rival) {
|
|
3411
|
+
process.stderr.write(
|
|
3412
|
+
`${pc.yellow(say("handoff.twoRecent"))}
|
|
3413
|
+
` + [first, rival].map((ref2) => ` ${say("handoff.candidate", { handle: ref2.handle, target: targetWord, agent: labelOf(ref2), when: sinceMs(ref2.updatedAt, now) })}`).join("\n") + "\n"
|
|
3414
|
+
);
|
|
3415
|
+
return 1;
|
|
3416
|
+
}
|
|
3417
|
+
const conversation2 = await read(first, store);
|
|
3418
|
+
if (typeof conversation2 === "number") return conversation2;
|
|
3419
|
+
const taken = say("handoff.tookNewest", { agent: labelOf(first), handle: first.handle, when: sinceMs(first.updatedAt, now) });
|
|
3420
|
+
if (!parsed.json) process.stderr.write(pc.dim(`${taken}
|
|
3421
|
+
`));
|
|
3422
|
+
return { conversation: conversation2, taken };
|
|
3423
|
+
}
|
|
3424
|
+
const discovery = await discover(store, { cwds: [cwd] });
|
|
3425
|
+
if (typeof discovery === "number") return discovery;
|
|
3426
|
+
let ref;
|
|
3427
|
+
try {
|
|
3428
|
+
ref = resolveConversation(source, discovery);
|
|
3429
|
+
} catch (error) {
|
|
3430
|
+
process.stderr.write(pc.red(`${handoffFaultText(error)}
|
|
3431
|
+
`));
|
|
3432
|
+
return 1;
|
|
3433
|
+
}
|
|
3434
|
+
const conversation = await read(ref, store);
|
|
3435
|
+
if (typeof conversation === "number") return conversation;
|
|
3436
|
+
return { conversation };
|
|
3437
|
+
}
|
|
3438
|
+
async function discover(store, extra) {
|
|
3439
|
+
try {
|
|
3440
|
+
return await discoverConversations({ ...store, ...extra });
|
|
3441
|
+
} catch (error) {
|
|
3442
|
+
process.stderr.write(pc.red(`${handoffFaultText(error)}
|
|
3443
|
+
`));
|
|
3444
|
+
return 1;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
async function read(ref, store) {
|
|
3448
|
+
try {
|
|
3449
|
+
return await readConversation(ref, store, { cwds: [ref.cwd] });
|
|
3450
|
+
} catch (error) {
|
|
3451
|
+
process.stderr.write(pc.red(`${handoffFaultText(error)}
|
|
3452
|
+
`));
|
|
3453
|
+
return 1;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
async function modelDigest(parsed, conversation) {
|
|
3457
|
+
let reply;
|
|
3458
|
+
try {
|
|
3459
|
+
reply = await catalogFetch(new URL("/api/handoff/digest", parsed.api), {
|
|
3460
|
+
method: "POST",
|
|
3461
|
+
headers: { "Content-Type": "application/json" },
|
|
3462
|
+
body: JSON.stringify({ id: conversation.id })
|
|
3463
|
+
});
|
|
3464
|
+
} catch {
|
|
3465
|
+
unreachable(parsed.api);
|
|
3466
|
+
process.stderr.write(pc.dim(`${say("handoff.needsCatalog")}
|
|
3467
|
+
`));
|
|
3468
|
+
return 1;
|
|
3469
|
+
}
|
|
3470
|
+
if (!reply.ok) {
|
|
3471
|
+
process.stderr.write(pc.red(`${say("handoff.digestRejected", { status: reply.status, detail: await refusalOf(reply) })}
|
|
3472
|
+
`));
|
|
3473
|
+
return 1;
|
|
3474
|
+
}
|
|
3475
|
+
const body = await reply.json().catch(() => void 0);
|
|
3476
|
+
const digest = body?.digest ?? (body?.by === "model" ? body : void 0);
|
|
3477
|
+
if (!digest || typeof digest.title !== "string") {
|
|
3478
|
+
process.stderr.write(pc.red(`${say("handoff.digestUnreadable")}
|
|
3479
|
+
`));
|
|
3480
|
+
return 1;
|
|
3481
|
+
}
|
|
3482
|
+
return digest;
|
|
3483
|
+
}
|
|
3484
|
+
function preview(parsed, conversation, digest, target, tier, out) {
|
|
3485
|
+
const fidelity = target !== void 0 && target !== "bundle" ? fidelityOf(target) : void 0;
|
|
3486
|
+
const sizeFacts = { turns: conversation.turns.length, tokens: digest.stats.estimatedTokens, bytes: conversation.bytes };
|
|
3487
|
+
if (parsed.json) {
|
|
3488
|
+
process.stdout.write(
|
|
3489
|
+
`${JSON.stringify({ dryRun: true, conversation: refOf2(conversation), digest, tier, target: target ?? null, fidelity: fidelity ?? null, size: sizeFacts, dropped: conversation.dropped, ...target === "bundle" ? { bundle: out ?? null } : {} }, null, 2)}
|
|
3490
|
+
`
|
|
3491
|
+
);
|
|
3492
|
+
return 0;
|
|
3493
|
+
}
|
|
3494
|
+
const lines = ["", ` ${pc.bold(say("handoff.preview"))}`, ...digestLines(conversation, digest, tier)];
|
|
3495
|
+
if (fidelity) {
|
|
3496
|
+
lines.push("", ` ${pc.bold(say("handoff.travels", { agent: AGENT_NAMES[fidelity.agent] }))}`);
|
|
3497
|
+
for (const item of fidelity.carries) lines.push(` ${pc.green("+")} ${item}`);
|
|
3498
|
+
lines.push(` ${pc.bold(say("handoff.stays"))}`);
|
|
3499
|
+
for (const item of fidelity.leaves) lines.push(` ${pc.dim("\u2212")} ${pc.dim(item)}`);
|
|
3500
|
+
}
|
|
3501
|
+
if (target === "bundle") lines.push("", ` ${pc.dim(out ? say("handoff.bundleWouldWrite", { path: out }) : say("handoff.bundleWouldPrint"))}`);
|
|
3502
|
+
lines.push("", ` ${sizeLine(sizeFacts)}`, ` ${pc.dim(leftBehind(conversation.dropped))}`, "");
|
|
3503
|
+
process.stdout.write(lines.join("\n"));
|
|
3504
|
+
return 0;
|
|
3505
|
+
}
|
|
3506
|
+
function digestLines(conversation, digest, tier) {
|
|
3507
|
+
const lines = [
|
|
3508
|
+
` ${say("handoff.source", { agent: labelOf(conversation), handle: pc.cyan(conversation.handle), title: digest.title })}`,
|
|
3509
|
+
` ${pc.dim(say("handoff.digestBy", { by: digest.by, tier }))}`,
|
|
3510
|
+
"",
|
|
3511
|
+
` ${say("handoff.goal", { goal: oneLine2(digest.goal, 160) })}`
|
|
3512
|
+
];
|
|
3513
|
+
if (digest.summary) lines.push(` ${pc.dim(say("handoff.sourceSummary"))}`);
|
|
3514
|
+
lines.push(
|
|
3515
|
+
` ${pc.dim(
|
|
3516
|
+
say("handoff.counts", {
|
|
3517
|
+
decisions: digest.decisions.length,
|
|
3518
|
+
files: digest.filesTouched.length,
|
|
3519
|
+
commands: digest.commandsRun.length,
|
|
3520
|
+
open: digest.openItems.length
|
|
3521
|
+
})
|
|
3522
|
+
)}`
|
|
3523
|
+
);
|
|
3524
|
+
return lines;
|
|
3525
|
+
}
|
|
3526
|
+
function sizeLine(facts) {
|
|
3527
|
+
return say("handoff.sizeLine", { n: facts.turns, k: Math.max(1, Math.round(facts.tokens / 1e3)), size: size(facts.bytes) });
|
|
3528
|
+
}
|
|
3529
|
+
function leftBehind(dropped) {
|
|
3530
|
+
const parts = [];
|
|
3531
|
+
for (const key of ["thinking", "images", "subagents", "offloaded", "secrets", "other"]) {
|
|
3532
|
+
const n = dropped[key];
|
|
3533
|
+
if (n > 0) parts.push(say(`handoff.left.${key}`, { n }));
|
|
3534
|
+
}
|
|
3535
|
+
return parts.length === 0 ? "" : say("handoff.leftBehind", { list: parts.join(", ") });
|
|
3536
|
+
}
|
|
3537
|
+
var Cover = class {
|
|
3538
|
+
count = 0;
|
|
3539
|
+
text(value) {
|
|
3540
|
+
const out = redactSecrets(value);
|
|
3541
|
+
if (out !== value) this.count += marks2(out) - marks2(value);
|
|
3542
|
+
return out;
|
|
3543
|
+
}
|
|
3544
|
+
/** Strings inside a tool input pass through; the structure stays as it was. */
|
|
3545
|
+
input(value) {
|
|
3546
|
+
if (typeof value === "string") return this.text(value);
|
|
3547
|
+
if (Array.isArray(value)) return value.map((item) => this.input(item));
|
|
3548
|
+
if (value && typeof value === "object") {
|
|
3549
|
+
const out = {};
|
|
3550
|
+
for (const [key, item] of Object.entries(value)) out[key] = this.input(item);
|
|
3551
|
+
return out;
|
|
3552
|
+
}
|
|
3553
|
+
return value;
|
|
3554
|
+
}
|
|
3555
|
+
part(part) {
|
|
3556
|
+
switch (part.kind) {
|
|
3557
|
+
case "text":
|
|
3558
|
+
case "summary":
|
|
3559
|
+
return { ...part, text: this.text(part.text) };
|
|
3560
|
+
case "tool_call":
|
|
3561
|
+
return { ...part, input: this.input(part.input) };
|
|
3562
|
+
case "tool_result":
|
|
3563
|
+
return { ...part, output: this.text(part.output) };
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
turn(turn) {
|
|
3567
|
+
return { ...turn, parts: turn.parts.map((part) => this.part(part)) };
|
|
3568
|
+
}
|
|
3569
|
+
digest(digest) {
|
|
3570
|
+
const list2 = (items) => items.map((item) => this.text(item));
|
|
3571
|
+
const exchange = {};
|
|
3572
|
+
if (digest.lastExchange.user !== void 0) exchange.user = this.text(digest.lastExchange.user);
|
|
3573
|
+
if (digest.lastExchange.assistant !== void 0) exchange.assistant = this.text(digest.lastExchange.assistant);
|
|
3574
|
+
return {
|
|
3575
|
+
...digest,
|
|
3576
|
+
title: this.text(digest.title),
|
|
3577
|
+
goal: this.text(digest.goal),
|
|
3578
|
+
...digest.summary !== void 0 ? { summary: this.text(digest.summary) } : {},
|
|
3579
|
+
decisions: list2(digest.decisions),
|
|
3580
|
+
filesTouched: list2(digest.filesTouched),
|
|
3581
|
+
commandsRun: list2(digest.commandsRun),
|
|
3582
|
+
openItems: list2(digest.openItems),
|
|
3583
|
+
lastExchange: exchange
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
};
|
|
3587
|
+
function marks2(text) {
|
|
3588
|
+
return text.split(REDACTED).length - 1;
|
|
3589
|
+
}
|
|
3590
|
+
function coverBundle(conversation, digest) {
|
|
3591
|
+
const cover = new Cover();
|
|
3592
|
+
const turns = conversation.turns.map((turn) => cover.turn(turn));
|
|
3593
|
+
const compactions = conversation.compactions.map((compaction) => ({ ...compaction, text: cover.text(compaction.text) }));
|
|
3594
|
+
const covered = {
|
|
3595
|
+
...conversation,
|
|
3596
|
+
...conversation.title !== void 0 ? { title: cover.text(conversation.title) } : {},
|
|
3597
|
+
turns,
|
|
3598
|
+
compactions,
|
|
3599
|
+
dropped: { ...conversation.dropped }
|
|
3600
|
+
};
|
|
3601
|
+
const coveredDigest = cover.digest(digest);
|
|
3602
|
+
covered.dropped.secrets += cover.count;
|
|
3603
|
+
return { conversation: covered, digest: coveredDigest };
|
|
3604
|
+
}
|
|
3605
|
+
var INSIDE = {
|
|
3606
|
+
"claude-cli": { binary: "claude", out: "/logout", in: "/login" }
|
|
3607
|
+
};
|
|
3608
|
+
async function sameAgent(parsed, conversation, cwd, platform) {
|
|
3609
|
+
const agent = conversation.agent;
|
|
3610
|
+
const resume = resumeOf(agent, conversation.sessionId, conversation.cwd, platform);
|
|
3611
|
+
const fork = forkOf(agent, conversation.sessionId, conversation.cwd, platform);
|
|
3612
|
+
const app = resumeInApp(agent, conversation.sessionId, conversation.cwd, platform);
|
|
3613
|
+
const signOut = SIGN_OUT[agent] ?? "";
|
|
3614
|
+
const signIn = SIGN_IN[agent] ?? "";
|
|
3615
|
+
const bundle = `panoma handoff ${conversation.handle} --to bundle --out <file>`;
|
|
3616
|
+
if (parsed.json) {
|
|
3617
|
+
process.stdout.write(
|
|
3618
|
+
`${JSON.stringify({ ok: true, sameAgent: true, agent, signOut, signIn, resume: resume ?? null, resumeInApp: app ?? null, fork: fork ?? null, bundle }, null, 2)}
|
|
3619
|
+
`
|
|
3620
|
+
);
|
|
3621
|
+
return 0;
|
|
3622
|
+
}
|
|
3623
|
+
const lines = ["", ` ${say("handoff.sameAgent")}`, ""];
|
|
3624
|
+
let n = 0;
|
|
3625
|
+
const step = (text) => {
|
|
3626
|
+
n += 1;
|
|
3627
|
+
lines.push(` ${n}. ${text}`);
|
|
3628
|
+
};
|
|
3629
|
+
for (const line2 of accountLines(agent)) step(line2);
|
|
3630
|
+
if (resume) {
|
|
3631
|
+
step(say("handoff.sameAgentResume", { line: pc.cyan(await lineFor(resume, conversation.cwd, cwd, platform)) }));
|
|
3632
|
+
if (app) {
|
|
3633
|
+
const name = app.app.name;
|
|
3634
|
+
lines.push(` ${say("handoff.sameAgentAppDoor", { app: name, line: pc.cyan(app.line) })}`);
|
|
3635
|
+
lines.push(` ${pc.dim(say(agent === "claude-cli" ? "handoff.sameAgentAppClaude" : "handoff.sameAgentAppCodex", { app: name }))}`);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
if (fork) step(say("handoff.sameAgentFork", { line: pc.cyan(await lineFor(fork, conversation.cwd, cwd, platform)) }));
|
|
3639
|
+
step(say("handoff.sameAgentBundle", { handle: conversation.handle }));
|
|
3640
|
+
lines.push("", ` ${pc.dim(say("handoff.sameAgentHome", { handle: conversation.handle, target: wordOf(agent) }))}`, "");
|
|
3641
|
+
process.stdout.write(lines.join("\n"));
|
|
3642
|
+
return 0;
|
|
3643
|
+
}
|
|
3644
|
+
function accountLines(agent) {
|
|
3645
|
+
const inside = INSIDE[agent];
|
|
3646
|
+
const withSlash = (command, slash) => inside ? say("handoff.sameAgentOrInside", { command: pc.cyan(command), slash: pc.cyan(slash), binary: inside.binary }) : pc.cyan(command);
|
|
3647
|
+
return [
|
|
3648
|
+
say("handoff.sameAgentSignOut", { agent: AGENT_NAMES[agent], command: withSlash(SIGN_OUT[agent] ?? "", inside?.out ?? "") }),
|
|
3649
|
+
say("handoff.sameAgentSignIn", { command: withSlash(SIGN_IN[agent] ?? "", inside?.in ?? "") })
|
|
3650
|
+
];
|
|
3651
|
+
}
|
|
3652
|
+
function doorLines(app, name, line2, agent) {
|
|
3653
|
+
const lines = [];
|
|
3654
|
+
if (app) {
|
|
3655
|
+
lines.push(` ${pc.bold(say("handoff.openInApp", { app: name }))}`, ` ${pc.cyan(app.line)}`, ` ${pc.dim(say("handoff.appFallback", { sentence: app.sentence }))}`);
|
|
3656
|
+
if (agent === "claude-cli") lines.push(` ${pc.dim(say("handoff.claudeTrust"))}`);
|
|
3657
|
+
if (line2) lines.push("", ` ${pc.bold(say("handoff.orTerminal"))}`, ` ${pc.cyan(line2)}`);
|
|
3658
|
+
} else {
|
|
3659
|
+
lines.push(` ${say("handoff.appMacOnly", { app: name })}`);
|
|
3660
|
+
if (line2) lines.push(` ${pc.cyan(line2)}`);
|
|
3661
|
+
}
|
|
3662
|
+
return lines;
|
|
3663
|
+
}
|
|
3664
|
+
async function write({ parsed, deps, conversation, digest, target, surface, named, tier, out, taken, cwd, store, now }) {
|
|
3665
|
+
let result;
|
|
3666
|
+
try {
|
|
3667
|
+
result = await handoff({
|
|
3668
|
+
conversation,
|
|
3669
|
+
target,
|
|
3670
|
+
surface,
|
|
3671
|
+
tier,
|
|
3672
|
+
digest,
|
|
3673
|
+
cwd: conversation.cwd,
|
|
3674
|
+
targetHome: parsed.targetHome === void 0 ? void 0 : resolve2(expandTilde(parsed.targetHome)),
|
|
3675
|
+
keepTurns: parsed.keep,
|
|
3676
|
+
out,
|
|
3677
|
+
now,
|
|
3678
|
+
random: deps.random,
|
|
3679
|
+
options: store
|
|
3680
|
+
});
|
|
3681
|
+
} catch (error) {
|
|
3682
|
+
process.stderr.write(pc.red(`${handoffFaultText(error)}
|
|
3683
|
+
`));
|
|
3684
|
+
return 1;
|
|
3685
|
+
}
|
|
3686
|
+
const steps = [];
|
|
3687
|
+
let importFault;
|
|
3688
|
+
if (target === "opencode" && result.steps.length > 0) {
|
|
3689
|
+
const command = await opencodeCommand(deps);
|
|
3690
|
+
if (command === void 0) {
|
|
3691
|
+
steps.push(...result.steps.map((step) => ({ step, ran: false })));
|
|
3692
|
+
} else {
|
|
3693
|
+
try {
|
|
3694
|
+
const exec = deps.exec ?? runImport;
|
|
3695
|
+
await exec(command, ["import", result.path], conversation.cwd);
|
|
3696
|
+
steps.push(...result.steps.map((step) => ({ step, ran: true })));
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
importFault = handoffFaultText(`import-command-failed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`);
|
|
3699
|
+
steps.push(...result.steps.map((step) => ({ step, ran: false })));
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
} else {
|
|
3703
|
+
steps.push(...result.steps.map((step) => ({ step, ran: false })));
|
|
3704
|
+
}
|
|
3705
|
+
const recorded = named ? await record(parsed, conversation, result, target, surface, tier) : "";
|
|
3706
|
+
const resume = result.resume ? await lineFor(result.resume, conversation.cwd, cwd, store.platform) : void 0;
|
|
3707
|
+
const account = target === conversation.agent && parsed.targetHome === void 0 && tier !== "brief";
|
|
3708
|
+
if (parsed.json) {
|
|
3709
|
+
process.stdout.write(
|
|
3710
|
+
`${JSON.stringify(
|
|
3711
|
+
{
|
|
3712
|
+
ok: importFault === void 0,
|
|
3713
|
+
result,
|
|
3714
|
+
resume: resume ?? null,
|
|
3715
|
+
account: account ? { signOut: SIGN_OUT[target] ?? "", signIn: SIGN_IN[target] ?? "" } : null,
|
|
3716
|
+
steps,
|
|
3717
|
+
recorded: recorded ?? null,
|
|
3718
|
+
importFault: importFault ?? null,
|
|
3719
|
+
taken: taken ?? null
|
|
3720
|
+
},
|
|
3721
|
+
null,
|
|
3722
|
+
2
|
|
3723
|
+
)}
|
|
3724
|
+
`
|
|
3725
|
+
);
|
|
3726
|
+
return importFault === void 0 ? 0 : 1;
|
|
3727
|
+
}
|
|
3728
|
+
const lines = [""];
|
|
3729
|
+
if (tier === "brief" || !isNativeTarget(target)) {
|
|
3730
|
+
lines.push(` ${pc.green("\u2713")} ${say("handoff.documentWritten", { path: pc.dim(result.path) })}`);
|
|
3731
|
+
} else {
|
|
3732
|
+
lines.push(` ${pc.green("\u2713")} ${say("handoff.written", { agent: labelOf({ agent: target, surface }), path: pc.dim(result.path) })}`);
|
|
3733
|
+
}
|
|
3734
|
+
lines.push(` ${pc.dim(say("handoff.digestBy", { by: digest.by, tier }))}`);
|
|
3735
|
+
lines.push(` ${pc.dim(sizeLine({ turns: result.turns, tokens: digest.stats.estimatedTokens, bytes: result.bytes }))}`);
|
|
3736
|
+
if (account) {
|
|
3737
|
+
lines.push("");
|
|
3738
|
+
accountLines(target).forEach((line2, index) => lines.push(` ${index + 1}. ${line2}`));
|
|
3739
|
+
}
|
|
3740
|
+
if (surface === "app" && tier !== "brief" && isNativeTarget(target)) {
|
|
3741
|
+
lines.push("", ...doorLines(result.resumeInApp, labelOf({ agent: target, surface }), resume, target));
|
|
3742
|
+
} else if (resume) lines.push("", ` ${pc.bold(say("handoff.resume"))}`, ` ${pc.cyan(resume)}`);
|
|
3743
|
+
if (steps.length > 0) {
|
|
3744
|
+
lines.push("", ` ${pc.bold(say("handoff.then"))}`);
|
|
3745
|
+
for (const { step, ran } of steps) {
|
|
3746
|
+
lines.push(` ${ran ? pc.green("\u2713") : pc.yellow("\u2192")} ${ran ? say("handoff.stepRan", { step }) : say("handoff.stepPending", { step })}`);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
if (importFault) lines.push(` ${pc.red(importFault)}`);
|
|
3750
|
+
if (target === "claude-cli" && surface === "cli" && tier !== "brief") lines.push(` ${pc.dim(say("handoff.claudeContinue"))}`);
|
|
3751
|
+
const left = leftBehind(result.dropped);
|
|
3752
|
+
if (left) lines.push("", ` ${pc.dim(left)}`);
|
|
3753
|
+
if (recorded === void 0) lines.push(` ${pc.dim(say("handoff.notRecorded"))}`);
|
|
3754
|
+
else if (recorded) lines.push(` ${pc.dim(say("handoff.recorded", { id: recorded }))}`);
|
|
3755
|
+
lines.push("");
|
|
3756
|
+
process.stdout.write(lines.join("\n"));
|
|
3757
|
+
return importFault === void 0 ? 0 : 1;
|
|
3758
|
+
}
|
|
3759
|
+
async function opencodeCommand(deps) {
|
|
3760
|
+
const detect = deps.detect ?? (async () => (await detectCliAgents(providersByAuth("cli"))).map((agent) => ({ id: agent.provider.id, installed: agent.installed, command: agent.command })));
|
|
3761
|
+
const found = (await detect()).find((agent) => agent.id === "opencode" && agent.installed && agent.command);
|
|
3762
|
+
return found?.command;
|
|
3763
|
+
}
|
|
3764
|
+
async function runImport(file, args, cwd) {
|
|
3765
|
+
const launch = resolveExecutable(file, args);
|
|
3766
|
+
await run(launch.file, launch.args, { cwd, timeout: 6e4 });
|
|
3767
|
+
}
|
|
3768
|
+
async function record(parsed, conversation, result, target, surface, tier) {
|
|
3769
|
+
try {
|
|
3770
|
+
const reply = await catalogFetch(new URL("/api/handoff/record", parsed.api), {
|
|
3771
|
+
method: "POST",
|
|
3772
|
+
headers: { "Content-Type": "application/json" },
|
|
3773
|
+
body: JSON.stringify({
|
|
3774
|
+
id: conversation.id,
|
|
3775
|
+
target,
|
|
3776
|
+
surface,
|
|
3777
|
+
tier,
|
|
3778
|
+
targetSessionId: result.sessionId,
|
|
3779
|
+
targetPath: result.path,
|
|
3780
|
+
sourceHash: conversation.hash,
|
|
3781
|
+
turns: result.turns,
|
|
3782
|
+
bytes: result.bytes,
|
|
3783
|
+
dropped: result.dropped
|
|
3784
|
+
})
|
|
3785
|
+
});
|
|
3786
|
+
if (!reply.ok) return void 0;
|
|
3787
|
+
const body = await reply.json().catch(() => void 0);
|
|
3788
|
+
return body?.receipt?.id ?? body?.id ?? "";
|
|
3789
|
+
} catch {
|
|
3790
|
+
return void 0;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
async function lineFor(resume, conversationCwd, cwd, platform) {
|
|
3794
|
+
return sameFolder(await realFolder(conversationCwd), cwd, platform) ? [resume.command, ...resume.args].join(" ") : resume.line;
|
|
3795
|
+
}
|
|
3796
|
+
async function inFolder(refs, cwd, platform) {
|
|
3797
|
+
const kept = [];
|
|
3798
|
+
for (const ref of refs) {
|
|
3799
|
+
if (ref.cwd && insideFolder(await realFolder(ref.cwd), cwd, platform)) kept.push(ref);
|
|
3800
|
+
}
|
|
3801
|
+
return kept;
|
|
3802
|
+
}
|
|
3803
|
+
function wordOf(agent, surface = "cli") {
|
|
3804
|
+
if (surface === "app" && APP_OF[agent]) return APP_OF[agent].id;
|
|
3805
|
+
return Object.entries(AGENT_WORDS).find(([, id]) => id === agent)?.[0] ?? agent;
|
|
3806
|
+
}
|
|
3807
|
+
function nearestWord(word) {
|
|
3808
|
+
let best;
|
|
3809
|
+
for (const candidate of TARGET_WORDS) {
|
|
3810
|
+
const distance = editDistance(word, candidate);
|
|
3811
|
+
if (distance <= 2 && (!best || distance < best.distance)) best = { word: candidate, distance };
|
|
3812
|
+
}
|
|
3813
|
+
return best?.word;
|
|
3814
|
+
}
|
|
3815
|
+
function editDistance(a, b) {
|
|
3816
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3817
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3818
|
+
const current = [i];
|
|
3819
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3820
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
3821
|
+
}
|
|
3822
|
+
previous = current;
|
|
3823
|
+
}
|
|
3824
|
+
return previous[b.length];
|
|
3825
|
+
}
|
|
3826
|
+
function refOf2(conversation) {
|
|
3827
|
+
const ref = {
|
|
3828
|
+
id: conversation.id,
|
|
3829
|
+
agent: conversation.agent,
|
|
3830
|
+
sessionId: conversation.sessionId,
|
|
3831
|
+
handle: conversation.handle,
|
|
3832
|
+
path: conversation.path,
|
|
3833
|
+
cwd: conversation.cwd,
|
|
3834
|
+
updatedAt: conversation.updatedAt,
|
|
3835
|
+
turnCount: conversation.turnCount,
|
|
3836
|
+
bytes: conversation.bytes,
|
|
3837
|
+
compacted: conversation.compacted
|
|
3838
|
+
};
|
|
3839
|
+
for (const key of ["gitBranch", "title", "model", "startedAt"]) {
|
|
3840
|
+
if (conversation[key] !== void 0) ref[key] = conversation[key];
|
|
3841
|
+
}
|
|
3842
|
+
if (conversation.surface !== void 0) ref.surface = conversation.surface;
|
|
3843
|
+
if (conversation.limit) ref.limit = conversation.limit;
|
|
3844
|
+
return ref;
|
|
3845
|
+
}
|
|
3846
|
+
function oneLine2(text, max) {
|
|
3847
|
+
const flat2 = text.replace(/\s+/g, " ").trim();
|
|
3848
|
+
return flat2.length > max ? `${flat2.slice(0, max - 1)}\u2026` : flat2;
|
|
3849
|
+
}
|
|
3850
|
+
async function refusalOf(response) {
|
|
3851
|
+
const text = await response.text().catch(() => "");
|
|
3852
|
+
try {
|
|
3853
|
+
const body = JSON.parse(text);
|
|
3854
|
+
const error = typeof body.error === "string" ? body.error : "";
|
|
3855
|
+
const hint = typeof body.hint === "string" ? ` ${body.hint}` : "";
|
|
3856
|
+
if (error) return `${error}${hint}`;
|
|
3857
|
+
} catch {
|
|
3858
|
+
}
|
|
3859
|
+
return text.split("\n")[0]?.trim() ?? "";
|
|
3860
|
+
}
|
|
3861
|
+
function size(bytes) {
|
|
3862
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3863
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
3864
|
+
if (bytes < 10 * 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
3865
|
+
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(0)} MB`;
|
|
3866
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
3867
|
+
}
|
|
3868
|
+
export {
|
|
3869
|
+
coverBundle,
|
|
3870
|
+
groupByProject,
|
|
3871
|
+
handoffCommand,
|
|
3872
|
+
labelOf,
|
|
3873
|
+
leftBehind,
|
|
3874
|
+
nearestWord,
|
|
3875
|
+
rowLines
|
|
3876
|
+
};
|