orc-ai 0.1.37 → 0.1.41
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 +37 -1
- package/dist/index.js +641 -584
- package/dist/skills/orc-platform/SKILL.md +7 -0
- package/dist/skills/orc-platform/references/execution.md +7 -1
- package/dist/skills/orc-platform/references/session-archive.md +199 -0
- package/dist/skills/orc-platform/scripts/sync-sessions.py +297 -0
- package/dist/web/assets/{Agents-BbR4Bppp.js → Agents-1lX_DRrR.js} +1 -1
- package/dist/web/assets/{Audit-BFK82hee.js → Audit-DnRP9Ivm.js} +1 -1
- package/dist/web/assets/{Dashboard-DJufUiXp.js → Dashboard-bT5SABu_.js} +1 -1
- package/dist/web/assets/{DocumentWorkspace-C6iWm8RY.js → DocumentWorkspace-BI4JWVsO.js} +1 -1
- package/dist/web/assets/{FileEditor-C5d4xs5E.js → FileEditor-Cb8uXr6e.js} +1 -1
- package/dist/web/assets/{JobWebhooks-DGM7EHVj.js → JobWebhooks-CYE2UA0V.js} +1 -1
- package/dist/web/assets/{Jobs-Cwhf6HrZ.js → Jobs-Fyd46CkI.js} +1 -1
- package/dist/web/assets/Knowledge-prZWODFP.js +28 -0
- package/dist/web/assets/{PackageCatalog-rlnJnlaQ.js → PackageCatalog-R8kcGP28.js} +4 -4
- package/dist/web/assets/{Pager-B53jtBdE.js → Pager-urh4-P2a.js} +1 -1
- package/dist/web/assets/{Rules-C5l03Rhx.js → Rules-if9ZzABl.js} +2 -2
- package/dist/web/assets/Sessions-AfC-wZoj.js +5 -0
- package/dist/web/assets/Settings-CNPE54fO.js +20 -0
- package/dist/web/assets/Skills-BJcE6uiQ.js +1 -0
- package/dist/web/assets/{SourceEditor-DW0vd3ce.js → SourceEditor-a2_IdCoT.js} +3 -3
- package/dist/web/assets/{Tasks-DEDs5HYJ.js → Tasks-COQCj4HF.js} +8 -13
- package/dist/web/assets/Terminals-D4K_u_ch.js +115 -0
- package/dist/web/assets/Terminals-DODPh4gr.css +1 -0
- package/dist/web/assets/{Webhooks-BopgoCAt.js → Webhooks-DvsCfbZY.js} +1 -1
- package/dist/web/assets/{abnfDiagram-VCTEODGH-99oetRq5.js → abnfDiagram-VCTEODGH-C1g9AmGT.js} +1 -1
- package/dist/web/assets/{arc-jz6C0oUp.js → arc-BX1-afFK.js} +1 -1
- package/dist/web/assets/{architectureDiagram-5GKGNRK7-CDnf-tTr.js → architectureDiagram-5GKGNRK7-DTDe767x.js} +3 -3
- package/dist/web/assets/{blockDiagram-I7D4REHJ-wUZULRmh.js → blockDiagram-I7D4REHJ-DJnvrUZM.js} +1 -1
- package/dist/web/assets/{c4Diagram-7LVT6UL2-BbTCN0gc.js → c4Diagram-7LVT6UL2-rpXv4dc9.js} +1 -1
- package/dist/web/assets/channel-DsRXlXQz.js +1 -0
- package/dist/web/assets/{chunk-2Q5K7J3B-DXQa_KS-.js → chunk-2Q5K7J3B-BEq6zYAO.js} +1 -1
- package/dist/web/assets/{chunk-5VM5RSS4-DKex8fM9.js → chunk-5VM5RSS4-kAS1IBM0.js} +1 -1
- package/dist/web/assets/{chunk-F27PBJKO-CV24Bpwp.js → chunk-F27PBJKO-BtG5IndH.js} +1 -1
- package/dist/web/assets/{chunk-IMKFNOWR-dgfQu6Cr.js → chunk-IMKFNOWR-C-NO8OSz.js} +1 -1
- package/dist/web/assets/{chunk-JWPE2WC7-rU75UmMf.js → chunk-JWPE2WC7-2IhqyMft.js} +1 -1
- package/dist/web/assets/{chunk-POPQ4Y6H-D05TMjhY.js → chunk-POPQ4Y6H-DmxK4niw.js} +1 -1
- package/dist/web/assets/{chunk-SVP7TREG-DCPR8a_3.js → chunk-SVP7TREG-BkVeMS09.js} +1 -1
- package/dist/web/assets/{chunk-TICWLB2K-14JxXrzn.js → chunk-TICWLB2K-DzlMwg1V.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-CU-iUgTI.js → chunk-XXDRQBXY-ttUQmtBw.js} +1 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-CZXMGXM8.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-CZXMGXM8.js +1 -0
- package/dist/web/assets/{cose-bilkent-JH36ORCC-DsK-Wgux.js → cose-bilkent-JH36ORCC-yhoqj6vM.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-CIbMF4yA.js → cynefin-OW5HDTMX-CQZuSJ5b.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-Dfa-crHB.js → cynefinDiagram-5FMLGOSQ-Dz-2nuEW.js} +1 -1
- package/dist/web/assets/{dagre-GXQ25YYZ-B4F2yY9x.js → dagre-GXQ25YYZ-DZJid-R_.js} +1 -1
- package/dist/web/assets/{diagram-S7CK7UJ4-f6czEOz8.js → diagram-S7CK7UJ4-DB-Uzpn6.js} +1 -1
- package/dist/web/assets/{diagram-UQ7AKVKN-Bzc5H6mj.js → diagram-UQ7AKVKN-BjIgjC39.js} +1 -1
- package/dist/web/assets/{diagram-VSXAHHWV-nchG2_is.js → diagram-VSXAHHWV-CjFlgbiX.js} +1 -1
- package/dist/web/assets/{diagram-VX7I27RA-DtoGnSm-.js → diagram-VX7I27RA-BlfR5PYu.js} +1 -1
- package/dist/web/assets/{diagram-Z3DM3KII-B5RB4-4l.js → diagram-Z3DM3KII-DL6_QJQh.js} +1 -1
- package/dist/web/assets/{download-CUHnwD4I.js → download-CK_EusLu.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-PWID7BFC-474F-fnY.js → ebnfDiagram-PWID7BFC-BjdizMG0.js} +1 -1
- package/dist/web/assets/{erDiagram-RLTQ6QDP-DjcLWB7r.js → erDiagram-RLTQ6QDP-BxMzPgpM.js} +1 -1
- package/dist/web/assets/{flowDiagram-HODETNUW-ClrSnJZC.js → flowDiagram-HODETNUW-C-HgPS9G.js} +1 -1
- package/dist/web/assets/folder-open-Bcqha5Pu.js +16 -0
- package/dist/web/assets/{ganttDiagram-EL5Y4UJY-DvUWwHi6.js → ganttDiagram-EL5Y4UJY-BEx6HKx_.js} +4 -4
- package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-DzQUHPow.js → gitGraphDiagram-WWUBYQGX-D9yMgjsA.js} +1 -1
- package/dist/web/assets/{highlighted-body-KPVGNVTW-BwSphjF-.js → highlighted-body-KPVGNVTW-Cdq0W8LG.js} +1 -1
- package/dist/web/assets/index-BqQDBYwx.js +461 -0
- package/dist/web/assets/index-D1sNa9P-.css +1 -0
- package/dist/web/assets/{infoDiagram-27XIBGKW-BXiAYW5v.js → infoDiagram-27XIBGKW-e_LgGqFP.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-5VMMS53U-DtkPoBMd.js → ishikawaDiagram-5VMMS53U-jOCokdCr.js} +1 -1
- package/dist/web/assets/{journeyDiagram-3NMN7TZE-De55MHfN.js → journeyDiagram-3NMN7TZE-DfyXRW30.js} +1 -1
- package/dist/web/assets/{kanban-definition-UXKFOSKX-CMbmC8bh.js → kanban-definition-UXKFOSKX-D6Mgx6_N.js} +1 -1
- package/dist/web/assets/{layout-CMoc3Ymp.js → layout-BFQQ8SSL.js} +1 -1
- package/dist/web/assets/{linear-ByaFbiCi.js → linear-C251cMFR.js} +1 -1
- package/dist/web/assets/list-BnZDr6iH.js +6 -0
- package/dist/web/assets/mermaid-HWGCJPDP-DE8bnMel.js +609 -0
- package/dist/web/assets/{mindmap-definition-YA3MSWOX-DHpoB_3Z.js → mindmap-definition-YA3MSWOX-hX6YQE6d.js} +1 -1
- package/dist/web/assets/{pegDiagram-XKGWAZYB-DlUFQicu.js → pegDiagram-XKGWAZYB-DRRv8KEz.js} +1 -1
- package/dist/web/assets/{pencil-C7MeJi2-.js → pencil-ClEwbF1x.js} +1 -1
- package/dist/web/assets/{pieDiagram-E7YTZNPT-BTxNRrLy.js → pieDiagram-E7YTZNPT-DwRQy5kr.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AXDQQJYC-BXisK15b.js → quadrantDiagram-AXDQQJYC-u-NZRX9E.js} +1 -1
- package/dist/web/assets/{railroadDiagram-O6MQD6OU-Dep-5EOi.js → railroadDiagram-O6MQD6OU-DsGn0lJc.js} +1 -1
- package/dist/web/assets/{refresh-cw-Bwp-x39v.js → refresh-cw-BGtNOi70.js} +1 -1
- package/dist/web/assets/{requirementDiagram-BXWQKSXE-Dts4RxIL.js → requirementDiagram-BXWQKSXE-Bak7hmyH.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-P5KCCOFB-B55oiprJ.js → sankeyDiagram-P5KCCOFB-Bx5Wxkio.js} +1 -1
- package/dist/web/assets/{save-F382q8r7.js → save-ChNqW56W.js} +1 -1
- package/dist/web/assets/{scroll-area-B2WVTNHY.js → scroll-area-YSA2ORI8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-Dm8OZOYP.js → sequenceDiagram-WJ2MYXX4-CyWelz_H.js} +1 -1
- package/dist/web/assets/{sizeCapture-INFHLROL-CdGYDQGd.js → sizeCapture-INFHLROL-BCzEf-8B.js} +1 -1
- package/dist/web/assets/{stateDiagram-D77RDMKH-DzLl6piI.js → stateDiagram-D77RDMKH-BkqZfGYQ.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-PLwI4Xlh.js +1 -0
- package/dist/web/assets/{swimlanes-42K2YHIH-C0y9oB16.js → swimlanes-42K2YHIH-BMYDAy8X.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-BEGqjIIe.js +8 -0
- package/dist/web/assets/{timeline-definition-24CTP7MA-BcFnACv9.js → timeline-definition-24CTP7MA-BrGykZXB.js} +1 -1
- package/dist/web/assets/{upload-De21en-z.js → upload-waJ36Q68.js} +1 -1
- package/dist/web/assets/{useJobs-C4BQQWOR.js → useJobs-By4K4BjX.js} +1 -1
- package/dist/web/assets/{useKnowledge-Dlk6zbmi.js → useKnowledge-DMSoPz-b.js} +1 -1
- package/dist/web/assets/{useMemories-B0YNmRrw.js → useMemories-bv4XvbzQ.js} +1 -1
- package/dist/web/assets/useProjectScope-BvcpSOql.js +1 -0
- package/dist/web/assets/{vennDiagram-4TSXK5OY-Bn7Y7h5d.js → vennDiagram-4TSXK5OY-CkLSasvk.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VM6X3IG4-Bd7zbrPy.js → wardleyDiagram-VM6X3IG4-DfPte2eh.js} +1 -1
- package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CyDYeK9G.js → xychartDiagram-S5SC5T6Z-ClFkeona.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/dist/web/assets/Knowledge-D5JF_DmG.js +0 -38
- package/dist/web/assets/Sessions-B1ES2jjN.js +0 -1
- package/dist/web/assets/Settings-DZTNnZp2.js +0 -12
- package/dist/web/assets/Skills-zUJSqzxy.js +0 -1
- package/dist/web/assets/Terminals-BbWYyHA3.css +0 -1
- package/dist/web/assets/Terminals-jLXfmfnA.js +0 -43
- package/dist/web/assets/channel-BMK2j5Xo.js +0 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-C3CpsbeZ.js +0 -1
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-C3CpsbeZ.js +0 -1
- package/dist/web/assets/folder-open-DqVAWgc5.js +0 -6
- package/dist/web/assets/index-2ojlkIJg.css +0 -1
- package/dist/web/assets/index-CRsafixF.js +0 -379
- package/dist/web/assets/mermaid-HWGCJPDP-BANMxb7n.js +0 -609
- package/dist/web/assets/schemas-Cwl03S4d.js +0 -57
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-Bd9-xfy5.js +0 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-CY_HM0pG.js +0 -8
- package/dist/web/assets/useProjectScope-NKMBMTCk.js +0 -1
|
@@ -18,6 +18,12 @@ installation as a side effect.
|
|
|
18
18
|
Read only the reference matching the request. Related operations share a file so
|
|
19
19
|
one recipe does not require a chain of tiny reads.
|
|
20
20
|
|
|
21
|
+
Bundled ORC skill scripts are trusted operational helpers. Use the matching
|
|
22
|
+
reference and `--help` (or the short file header) rather than reading their full
|
|
23
|
+
source into context. Read the source for debugging or when a script comes from
|
|
24
|
+
an untrusted source. Signed downloads of bundled scripts retain this workflow;
|
|
25
|
+
verify the returned hash/size without loading the script body into context.
|
|
26
|
+
|
|
21
27
|
| Request | Reference |
|
|
22
28
|
| --- | --- |
|
|
23
29
|
| Install, connect an agent, CLI/API/MCP, configure auth, diagnose connectivity | [Setup and interfaces](references/setup.md) |
|
|
@@ -25,6 +31,7 @@ one recipe does not require a chain of tiny reads.
|
|
|
25
31
|
| KB intake/search/edit, parsers, embeddings, README/Markdown and living Learnings digests | [Knowledge and documentation](references/knowledge.md) |
|
|
26
32
|
| Projects, tasks/flows, jobs, gateway, status, tags and audit | [Work and operations](references/work.md) |
|
|
27
33
|
| Session continuity/handoff, terminals, chat, SSH execution, Git/GitHub | [Sessions and execution](references/execution.md) |
|
|
34
|
+
| Archive local coding sessions on remote ORC, incremental sync/deduplication, session retrieval design | [Session archive and search](references/session-archive.md) |
|
|
28
35
|
| Deterministic rules, hooks and enforcement capabilities | [Rules](references/rules.md) |
|
|
29
36
|
| Author/import/edit skills, agent profiles and packages | [Reusable definitions](references/packages.md) |
|
|
30
37
|
|
|
@@ -4,6 +4,9 @@ Sources: [Live sessions](https://github.com/niradler/orc/blob/master/docs/live-s
|
|
|
4
4
|
|
|
5
5
|
## Session continuity
|
|
6
6
|
|
|
7
|
+
For bulk/offline local history sync, use [session-archive.md](session-archive.md).
|
|
8
|
+
It stores histories without running agents; the handoff below moves active work.
|
|
9
|
+
|
|
7
10
|
Use the supplied ORC session ID. At meaningful milestones record decisions, failures,
|
|
8
11
|
artifact/commit references and next action with `session_event`; avoid per-line
|
|
9
12
|
noise. `session_snapshot` preserves compact context; after compaction use
|
|
@@ -63,7 +66,10 @@ an ORC event snapshot. Parent sessions are required for Codex child-agent histor
|
|
|
63
66
|
`${CODEX_HOME:-~/.codex}/sessions/YYYY/MM/DD/rollout-*-<session-id>.jsonl`
|
|
64
67
|
or an explicitly selected archive. Use the actual parent native session.
|
|
65
68
|
5. Confirm `status: ready`, destination cwd and `live_session_id`, then use
|
|
66
|
-
**Sessions → Open in terminal**.
|
|
69
|
+
**Sessions → Open in terminal**. If the user requested move-and-continue, finish
|
|
70
|
+
the launch through `POST /api/terminals` with the agent kind and live_session_id
|
|
71
|
+
and verify the running terminal/cwd/host. Do not leave the user at Ready.
|
|
72
|
+
Upload acceptance alone does not prove readiness or continuation.
|
|
67
73
|
|
|
68
74
|
Inspect `GET /api/session-handoffs/<id>` on failure. After correcting a preparation
|
|
69
75
|
failure, retry `POST /api/session-handoffs/<id>/prepare` using normal auth. A consumed
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Coding session archive, remote sync and retrieval
|
|
2
|
+
|
|
3
|
+
Status: implemented in this feature checkout; use the selected installation's
|
|
4
|
+
schemas/help to confirm its version includes these operations. Archive sync and
|
|
5
|
+
native continuation are two separate user outcomes. The design rationale below
|
|
6
|
+
also records limits and future improvements; it is not permission to deploy.
|
|
7
|
+
|
|
8
|
+
## Sync local histories for offline retrieval
|
|
9
|
+
|
|
10
|
+
Use this when asked to upload this project's sessions, all Claude sessions or a
|
|
11
|
+
selected collection to remote ORC. The completed outcome is searchable history in
|
|
12
|
+
**Sessions → Archived sessions**, available while the PC is offline. **Show ended
|
|
13
|
+
is selected by default**; explicitly hiding ended sessions also hides the archive.
|
|
14
|
+
Archival creates neither a native agent process nor a worktree.
|
|
15
|
+
|
|
16
|
+
1. Select the intended ORC installation and existing project(s). Call
|
|
17
|
+
`session_sync({projects:["my-app"]})` for one/more requested projects, or
|
|
18
|
+
`session_sync({})` for an all-project history request. It returns a private,
|
|
19
|
+
one-hour sync grant with covered projects. Pass the result to the helper via
|
|
20
|
+
`--grant-stdin` or a private JSON file with `--grant-file`; do not print its URL,
|
|
21
|
+
put it in argv/history or commit the grant file. The main API secret is not needed.
|
|
22
|
+
For hosted ORC behind a TLS proxy, preserve the external `Host` and set
|
|
23
|
+
`X-Forwarded-Proto: https`; grants and signed script downloads then use the
|
|
24
|
+
externally reachable HTTPS origin. Do not expose the internal API port publicly.
|
|
25
|
+
For a single project, use
|
|
26
|
+
its readable name and the local cwd. For an all-agent/history request, discover
|
|
27
|
+
remote projects and use their configured Git remote identities or a small
|
|
28
|
+
explicit cwd/repository-to-project JSON map. Unmapped histories stay local and
|
|
29
|
+
are reported; do not guess a project from a directory basename.
|
|
30
|
+
2. Use trusted `scripts/sync-sessions.py` from the orc-platform root. Do not read
|
|
31
|
+
its full source. If absent locally, call
|
|
32
|
+
`skill_read({name:"orc-platform",ref:"scripts/sync-sessions.py",download:true,project})`,
|
|
33
|
+
download the signed bytes privately and verify the supplied SHA-256/size. Run
|
|
34
|
+
`python scripts/sync-sessions.py --help` for current flags. Downloading does not
|
|
35
|
+
execute the script. A configured standalone client may alternatively use
|
|
36
|
+
`ORC_API_BASE` and `ORC_API_SECRET` from its secure environment, never literal argv.
|
|
37
|
+
3. Preview with `--dry-run`, then sync the requested scope. Examples using the
|
|
38
|
+
private grant JSON returned to the connected agent:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
python scripts/sync-sessions.py --grant-file /private/sync-grant.json --project my-app --cwd /local/repo --dry-run
|
|
42
|
+
python scripts/sync-sessions.py --grant-file /private/sync-grant.json --project my-app --cwd /local/repo
|
|
43
|
+
python scripts/sync-sessions.py --grant-file /private/sync-grant.json --agent claude --project-map /local/project-map.json
|
|
44
|
+
python scripts/sync-sessions.py --grant-file /private/sync-grant.json --agent codex --project my-app --session-file /local/rollout.jsonl
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For Windows, quote a cwd such as `"C:\Projects\my-app"`. A map is an object
|
|
48
|
+
such as `{"C:\\Projects\\my-app":"my-app","https://github.com/org/repo":"another-app"}`.
|
|
49
|
+
`--project` explicitly assigns every selected session to that project; combine
|
|
50
|
+
it with `--cwd` for a project-only request. For collections spanning projects,
|
|
51
|
+
prefer remote matching/mapping. `--root` overrides a native root for one agent;
|
|
52
|
+
`--exclude` is repeatable. Default Claude discovery uses `CLAUDE_CONFIG_DIR/projects`;
|
|
53
|
+
Codex uses `CODEX_HOME/sessions` and `archived_sessions`, with normal home fallbacks.
|
|
54
|
+
4. Inspect per-session outcomes and final counts. Nonzero exit means failed,
|
|
55
|
+
unmapped or conflicting items need attention; successful items remain stored.
|
|
56
|
+
Repeat the command after correcting those items. The script persists a stable
|
|
57
|
+
source UUID and receipts under `~/.orc`, verifies unchanged receipts against the
|
|
58
|
+
remote manifest, retries transient requests and uploads changed snapshots only.
|
|
59
|
+
It extracts native identity, parent/version, cwd, available repository/branch/commit
|
|
60
|
+
and timestamps; it does not invent a historical commit from today's checkout.
|
|
61
|
+
A changing file is retried later, and an unfinished last JSONL line is held back.
|
|
62
|
+
5. Verify Sessions for the selected project, search a known transcript phrase and
|
|
63
|
+
open the saved history. The archive API exposes lexical index revision, pending
|
|
64
|
+
embedding chunks and errors separately. Use `--reindex` to resend a snapshot and
|
|
65
|
+
retry its index without duplicating the archive session. No local ORC database
|
|
66
|
+
or SSH connection from the server to the PC is required.
|
|
67
|
+
|
|
68
|
+
Native bundles are bounded to 64 MiB per encoded request and 128 files. Claude
|
|
69
|
+
subagent sidecars are included; configuration, credentials, SQLite databases and
|
|
70
|
+
repository files are not uploaded. Large collections use bounded concurrency and
|
|
71
|
+
per-session checkpoints; a single file over the limit is reported, not split into
|
|
72
|
+
an invalid native session. Resumable multipart uploads are future work.
|
|
73
|
+
|
|
74
|
+
## Move a session and continue on ORC
|
|
75
|
+
|
|
76
|
+
Use this when the user wants to close the PC and continue the work on the ORC
|
|
77
|
+
execution host. The outcome is a **running resumed terminal**, not just stored
|
|
78
|
+
history or a Ready badge. Keep the source idle during the final transfer.
|
|
79
|
+
|
|
80
|
+
- For a currently local session, follow [execution.md](execution.md)'s handoff:
|
|
81
|
+
within the user's Git authorization commit/push the intended code, verify its
|
|
82
|
+
exact remote SHA, call `session_handoff`, then use trusted
|
|
83
|
+
`scripts/upload-session.py` with the single-use upload URL. Use its instructions
|
|
84
|
+
and `--help`; full script inspection is for debugging/untrusted code.
|
|
85
|
+
- For an already archived session, use its **Prepare and resume in terminal**
|
|
86
|
+
action. Supply/verify the pushed repository, branch and commit where history
|
|
87
|
+
lacks them. The server uses the existing handoff import/checks and keeps the
|
|
88
|
+
archive revision and handoff link. Preparation copies history to a new native ID
|
|
89
|
+
and pinned worktree; the source archive remains intact.
|
|
90
|
+
- After status is `ready`, an agent completing a move-and-continue request calls
|
|
91
|
+
authenticated `POST /api/terminals` with `{kind:"claude"|"codex",live_session_id}`
|
|
92
|
+
and verifies the returned terminal/cwd/host. The UI action performs this launch
|
|
93
|
+
automatically. Do not stop at upload acceptance or Ready when continuation was
|
|
94
|
+
requested. Destination agent credentials/dependencies must already be available;
|
|
95
|
+
ignored files and running local processes are not transferred.
|
|
96
|
+
|
|
97
|
+
## Settings and retrieval APIs
|
|
98
|
+
|
|
99
|
+
Settings → Knowledge has **Search coding sessions with embeddings**, off by
|
|
100
|
+
default. It uses the existing configured embedding provider. Enabling also indexes
|
|
101
|
+
discovered Claude/Codex sessions already assigned to projects on the selected host;
|
|
102
|
+
**Index discovered sessions** can index those histories manually without vectors.
|
|
103
|
+
The existing native metadata/ripgrep search and paged transcripts remain available.
|
|
104
|
+
Uploads additionally have a persistent server-side keyword index, so fallback
|
|
105
|
+
does not depend on the original PC. Provider failures/pending vectors are visible.
|
|
106
|
+
|
|
107
|
+
| Operation | API |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| Mint scoped connected-agent sync grant | `POST /api/sessions/archive/sync` |
|
|
110
|
+
| Upload one snapshot | `POST /api/sessions/archive/upload` |
|
|
111
|
+
| Compare up to 100 origins | `POST /api/sessions/archive/manifest` |
|
|
112
|
+
| List/search bounded hits | `GET /api/sessions/archive?q=...&project_id=...&limit=20&offset=0` |
|
|
113
|
+
| Index discovered project histories | `POST /api/sessions/archive/index` |
|
|
114
|
+
| Metadata, revisions and handoff links | `GET /api/sessions/archive/{id}` |
|
|
115
|
+
| Pinned conversation page | `GET /api/sessions/archive/{id}/transcript?revision=1&offset=0&limit=100` |
|
|
116
|
+
| Resolve a passage | `GET /api/sessions/archive/{id}/passage?revision=1&ordinal=0` |
|
|
117
|
+
| Original native bundle | `GET /api/sessions/archive/{id}/export?revision=1` |
|
|
118
|
+
| Prepare native continuation | `POST /api/sessions/archive/{id}/resume` |
|
|
119
|
+
|
|
120
|
+
Normal installation API authentication applies. Remote hosting must configure a
|
|
121
|
+
secret and HTTPS. The grant's private `/api/session-sync/<token>` base permits only
|
|
122
|
+
its scoped project listing, manifest checks and uploads; it cannot read transcripts,
|
|
123
|
+
mint grants or launch terminals. An expired grant needs a new `session_sync` call;
|
|
124
|
+
reruns retain the local origin/receipts and do not duplicate acknowledged history.
|
|
125
|
+
Upload includes required readable `project`, stable `source_id`,
|
|
126
|
+
`agent`, native `session_id`, `title`, `metadata` and the existing version-1 native
|
|
127
|
+
`bundle`. Optional `expected_revision` rejects stale writers; verified identical or
|
|
128
|
+
older prefixes remain no-ops. A conflict returns HTTP409 with its retained revision;
|
|
129
|
+
it never replaces current history. Do not blindly retry divergence as an overwrite.
|
|
130
|
+
|
|
131
|
+
MCP `session_sync` supplies the uploader capability. `session_search` returns bounded cited passages; `session_read` fetches a pinned
|
|
132
|
+
passage or message page. Citations are `session:<archive-id>@<revision>#<ordinal>`.
|
|
133
|
+
The SDK provides archive search/upload/index/read/passage/resume under `sessions`.
|
|
134
|
+
Search returns at most three passages per session; no whole history is included.
|
|
135
|
+
Current archive list/search pagination uses limit/offset, not snapshot cursors.
|
|
136
|
+
|
|
137
|
+
The remaining sections explain the architecture and future scale decisions.
|
|
138
|
+
|
|
139
|
+
## Identity, updates and retrieval guarantees
|
|
140
|
+
|
|
141
|
+
The origin key is (source installation ID, agent, native session UUID), independent
|
|
142
|
+
of filename, cwd and project labels. The uploader persists its installation UUID;
|
|
143
|
+
reinstallations should retain it. A copy from another PC has a different origin
|
|
144
|
+
unless the operator explicitly uses the same source ID. A native ID alone never
|
|
145
|
+
merges unrelated sessions. Original native files remain untouched.
|
|
146
|
+
|
|
147
|
+
- Identical canonical records: return the same archive ID/revision; no re-embedding.
|
|
148
|
+
- Verified extension of every existing file's ordered records: advance the same
|
|
149
|
+
archive to a new immutable revision and reuse unchanged chunk embeddings.
|
|
150
|
+
- Older verified prefix: stale/no-op; never truncate current history.
|
|
151
|
+
- Divergence, rewritten records, missing earlier sidecars or an incompatible
|
|
152
|
+
expected revision: retain the conflicting snapshot, return HTTP409 and leave
|
|
153
|
+
the canonical head intact. Conflicting replay is idempotent. Resolve explicitly;
|
|
154
|
+
no timestamp-based concatenation or last-write-wins.
|
|
155
|
+
- Native continuation creates a new session ID; an archive-to-handoff relation
|
|
156
|
+
preserves its source revision. It never rewrites the archived original.
|
|
157
|
+
|
|
158
|
+
Each revision records raw native files in private blob storage alongside the
|
|
159
|
+
knowledge root. Metadata/revision pointers live in orc.db. Lexical chunks and
|
|
160
|
+
vectors use a separate rebuildable session index, sharing the existing KB index
|
|
161
|
+
implementation and provider/profile cache. Lost indexes rebuild from originals;
|
|
162
|
+
unfinished embedding work resumes from durable pending chunks. Indexing and
|
|
163
|
+
search never execute historical tool calls or trust transcript instructions.
|
|
164
|
+
|
|
165
|
+
Passages are turn-aware, split at 1,600 characters after the existing bounded
|
|
166
|
+
transcript normalization, and include Claude sidecar locators. Raw exports retain
|
|
167
|
+
the uploaded native text rather than the bounded display projection. Search groups at most three cited passages
|
|
168
|
+
per session, with separate pinned revision/message-page reads. Model/profile
|
|
169
|
+
changes backfill compatible vectors; provider failure preserves keyword results.
|
|
170
|
+
Embedding batches are internal provider requests, not whole-document results.
|
|
171
|
+
|
|
172
|
+
Native metadata/ripgrep search remains available alongside archive retrieval.
|
|
173
|
+
KB search still returns document metadata and cited passages, with kb_read for
|
|
174
|
+
more context and kb_evidence for token-budgeted evidence. It is also top-k,
|
|
175
|
+
not cursor-paginated. Archive pagination currently uses bounded limit/offset;
|
|
176
|
+
concurrent updates can reorder pages. Snapshot cursors, resumable part uploads,
|
|
177
|
+
retention/garbage collection and interactive conflict-resolution tooling are
|
|
178
|
+
future improvements, not claims about the current implementation.
|
|
179
|
+
|
|
180
|
+
## Operational boundaries
|
|
181
|
+
|
|
182
|
+
Require HTTPS and configured API authentication for remote hosting. Existing
|
|
183
|
+
shared API-secret access is installation access, not per-user tenant isolation.
|
|
184
|
+
Select explicit project scope for retrieval. Session text can contain private
|
|
185
|
+
code or secrets: exclude/redact at the source as needed, and enable external
|
|
186
|
+
embeddings only with a provider approved for that text. Credentials/config and
|
|
187
|
+
agent databases are never selected as native session inputs.
|
|
188
|
+
|
|
189
|
+
Uploads count actual streamed bytes, enforce format/identity/path limits, and
|
|
190
|
+
never write to uploader-supplied filesystem destinations. Failed network retries
|
|
191
|
+
and stale snapshots cannot replace current data. Check storage/index/provider
|
|
192
|
+
states separately. A searchable archive without available code or destination
|
|
193
|
+
credentials remains readable/exportable; it is not automatically resumable.
|
|
194
|
+
|
|
195
|
+
Verify both outcomes proportionally: repeat/increment/conflict/recovery tests for
|
|
196
|
+
archive sync; then a real browser flow from saved history through pinned Git
|
|
197
|
+
preparation to a terminal with the expected host/cwd and native session. Local
|
|
198
|
+
isolated E2E evidence does not establish a hosted production deployment or a
|
|
199
|
+
successful paid model turn.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Sync local Claude/Codex histories to ORC without launching agents.
|
|
2
|
+
|
|
3
|
+
Use --help for project/cwd/agent selection. Authentication comes from ORC_API_SECRET,
|
|
4
|
+
never argv. --dry-run previews mapping. Source files and agent credentials are untouched.
|
|
5
|
+
Repeated snapshots are idempotent; divergent revisions are retained as conflicts.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
import datetime
|
|
11
|
+
import functools
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import pathlib
|
|
16
|
+
import re
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import time
|
|
20
|
+
import urllib.error
|
|
21
|
+
import urllib.parse
|
|
22
|
+
import urllib.request
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NoRedirects(urllib.request.HTTPRedirectHandler):
|
|
27
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
28
|
+
raise ValueError("ORC redirects refused; use the final API URL")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def request(base, path, body=None):
|
|
32
|
+
if "/api/session-sync/" in base:
|
|
33
|
+
path = path.removeprefix("/sessions/archive")
|
|
34
|
+
headers = {"Content-Type": "application/json", "x-orc-source": "cli"}
|
|
35
|
+
token = None if "/api/session-sync/" in base else os.environ.get("ORC_API_SECRET")
|
|
36
|
+
if token:
|
|
37
|
+
headers["Authorization"] = "Bearer " + token
|
|
38
|
+
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
39
|
+
if data is not None and len(data) > 64 * 1024 * 1024:
|
|
40
|
+
raise ValueError("Encoded session exceeds the 64 MiB limit")
|
|
41
|
+
req = urllib.request.Request(base + path, data=data, headers=headers)
|
|
42
|
+
for attempt in range(3):
|
|
43
|
+
try:
|
|
44
|
+
with urllib.request.build_opener(NoRedirects()).open(req, timeout=180) as response:
|
|
45
|
+
return json.load(response)
|
|
46
|
+
except urllib.error.HTTPError as error:
|
|
47
|
+
if error.code == 409:
|
|
48
|
+
return json.load(error)
|
|
49
|
+
if error.code not in (429, 502, 503, 504) or attempt == 2:
|
|
50
|
+
raise ValueError(f"ORC HTTP {error.code}; check API access and project mapping") from None
|
|
51
|
+
except (urllib.error.URLError, TimeoutError):
|
|
52
|
+
if attempt == 2:
|
|
53
|
+
raise ValueError("ORC connection failed after three attempts") from None
|
|
54
|
+
time.sleep(2 ** attempt)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def snapshot(path):
|
|
58
|
+
before = path.stat()
|
|
59
|
+
if before.st_size > 60 * 1024 * 1024:
|
|
60
|
+
raise ValueError("Native file exceeds 60 MiB; select a smaller session")
|
|
61
|
+
text = path.read_text(encoding="utf-8-sig")
|
|
62
|
+
after = path.stat()
|
|
63
|
+
if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
|
|
64
|
+
raise ValueError("Session changed while reading; retry once it is idle")
|
|
65
|
+
lines = text.splitlines()
|
|
66
|
+
if lines and not text.endswith("\n"):
|
|
67
|
+
try:
|
|
68
|
+
json.loads(lines[-1])
|
|
69
|
+
except ValueError:
|
|
70
|
+
lines.pop()
|
|
71
|
+
rows = [json.loads(line) for line in lines if line.strip()]
|
|
72
|
+
if not rows or any(not isinstance(row, dict) for row in rows):
|
|
73
|
+
raise ValueError("Expected nonempty native JSONL objects")
|
|
74
|
+
return "\n".join(lines) + "\n", rows
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def repository_key(value):
|
|
78
|
+
value = value.strip().rstrip("/").removesuffix(".git")
|
|
79
|
+
if value.startswith("git@") and ":" in value:
|
|
80
|
+
value = value[4:].replace(":", "/", 1)
|
|
81
|
+
parsed = urllib.parse.urlparse(value)
|
|
82
|
+
port = (":" + str(parsed.port)) if parsed.port else ""
|
|
83
|
+
return ((parsed.hostname or "") + port + parsed.path if parsed.scheme else value).casefold()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def normalized_path(value):
|
|
87
|
+
text = value.replace("\\", "/")
|
|
88
|
+
if text.startswith("//?/"):
|
|
89
|
+
text = text[4:]
|
|
90
|
+
if len(text) > 1 and text[1] == ":":
|
|
91
|
+
text = text.casefold()
|
|
92
|
+
return text.rstrip("/")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@functools.lru_cache(maxsize=4096)
|
|
96
|
+
def local_repository(cwd):
|
|
97
|
+
if not cwd or not pathlib.Path(cwd).is_dir():
|
|
98
|
+
return None, ""
|
|
99
|
+
try:
|
|
100
|
+
remote = subprocess.run(["git", "-C", cwd, "remote", "get-url", "origin"], capture_output=True, text=True, timeout=5, check=True).stdout.strip()
|
|
101
|
+
prefix = subprocess.run(["git", "-C", cwd, "rev-parse", "--show-prefix"], capture_output=True, text=True, timeout=5, check=True).stdout.strip().rstrip("/")
|
|
102
|
+
return remote, prefix
|
|
103
|
+
except (OSError, subprocess.SubprocessError):
|
|
104
|
+
return None, ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def identify(agent, path, text, rows):
|
|
108
|
+
if agent == "codex":
|
|
109
|
+
meta = rows[0].get("payload", {})
|
|
110
|
+
if rows[0].get("type") != "session_meta" or not isinstance(meta, dict):
|
|
111
|
+
raise ValueError("Codex file lacks session_meta")
|
|
112
|
+
native_id = meta.get("id")
|
|
113
|
+
metadata = {"cwd": meta.get("cwd", ""), "subdirectory": ""}
|
|
114
|
+
if meta.get("cli_version"):
|
|
115
|
+
metadata["agent_version"] = str(meta["cli_version"])
|
|
116
|
+
if meta.get("parent_thread_id"):
|
|
117
|
+
metadata["parent_id"] = str(meta["parent_thread_id"])
|
|
118
|
+
git = meta.get("git", {})
|
|
119
|
+
if isinstance(git, dict):
|
|
120
|
+
for key, source in [("repository", "repository_url"), ("branch", "branch"), ("commit", "commit_hash")]:
|
|
121
|
+
if git.get(source):
|
|
122
|
+
metadata[key] = git[source]
|
|
123
|
+
else:
|
|
124
|
+
native = next((r for r in rows if r.get("sessionId") and r.get("type") in ("user", "assistant")), None)
|
|
125
|
+
if not native:
|
|
126
|
+
raise ValueError("Claude file lacks native conversation metadata")
|
|
127
|
+
native_id = native["sessionId"]
|
|
128
|
+
metadata = {"cwd": native.get("cwd", ""), "subdirectory": ""}
|
|
129
|
+
if native.get("version"):
|
|
130
|
+
metadata["agent_version"] = str(native["version"])
|
|
131
|
+
if native.get("gitBranch"):
|
|
132
|
+
metadata["branch"] = str(native["gitBranch"])
|
|
133
|
+
uuid.UUID(native_id)
|
|
134
|
+
cwd = metadata.get("cwd")
|
|
135
|
+
remote, prefix = local_repository(cwd)
|
|
136
|
+
if remote and "repository" not in metadata:
|
|
137
|
+
metadata["repository"] = remote
|
|
138
|
+
metadata["subdirectory"] = prefix
|
|
139
|
+
if metadata.get("repository"):
|
|
140
|
+
parsed = urllib.parse.urlparse(metadata["repository"])
|
|
141
|
+
if parsed.scheme in ("http", "https"):
|
|
142
|
+
metadata["repository"] = urllib.parse.urlunparse(parsed._replace(netloc=parsed.hostname + (":" + str(parsed.port) if parsed.port else ""), query="", fragment=""))
|
|
143
|
+
files = [{"path": "session.jsonl", "content": text}]
|
|
144
|
+
sidecars = path.parent / path.stem / "subagents"
|
|
145
|
+
if agent == "claude" and sidecars.is_dir():
|
|
146
|
+
for sidecar in sorted(sidecars.glob("agent-*.jsonl")):
|
|
147
|
+
if sidecar.is_symlink():
|
|
148
|
+
raise ValueError("Symbolic-link sidecars are not supported")
|
|
149
|
+
files.append({"path": "subagents/" + sidecar.name, "content": snapshot(sidecar)[0]})
|
|
150
|
+
if len(files) > 128:
|
|
151
|
+
raise ValueError("Too many sidecars")
|
|
152
|
+
title = path.stem
|
|
153
|
+
for row in rows:
|
|
154
|
+
message = row.get("message") or row.get("payload") or {}
|
|
155
|
+
if not isinstance(message, dict):
|
|
156
|
+
continue
|
|
157
|
+
content = message.get("content") if isinstance(message, dict) else None
|
|
158
|
+
if isinstance(content, list):
|
|
159
|
+
content = " ".join(str(b.get("text", "")) for b in content if isinstance(b, dict))
|
|
160
|
+
if (row.get("type") == "user" or message.get("role") == "user") and isinstance(content, str) and content.strip():
|
|
161
|
+
title = " ".join(content.split())[:200]
|
|
162
|
+
break
|
|
163
|
+
timestamps = []
|
|
164
|
+
for row in rows:
|
|
165
|
+
stamp = row.get("timestamp")
|
|
166
|
+
if isinstance(stamp, str):
|
|
167
|
+
try:
|
|
168
|
+
date = datetime.datetime.fromisoformat(stamp.replace("Z", "+00:00"))
|
|
169
|
+
if date.tzinfo:
|
|
170
|
+
timestamps.append(date.astimezone(datetime.timezone.utc).isoformat())
|
|
171
|
+
except ValueError:
|
|
172
|
+
pass
|
|
173
|
+
if timestamps:
|
|
174
|
+
metadata.update(started_at=min(timestamps), ended_at=max(timestamps))
|
|
175
|
+
return {"agent": agent, "session_id": native_id, "title": title[:200], "metadata": metadata, "bundle": {"version": 1, "files": files}}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main():
|
|
179
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
180
|
+
parser.add_argument("--url", default=os.environ.get("ORC_API_BASE"), help="ORC server base URL; HTTPS required except loopback")
|
|
181
|
+
parser.add_argument("--grant-file", type=pathlib.Path, help="Private JSON returned by the connected agent's session_sync tool; no main API secret needed")
|
|
182
|
+
parser.add_argument("--grant-stdin", action="store_true", help="Read session_sync grant JSON from stdin instead of a file")
|
|
183
|
+
parser.add_argument("--project", help="Existing ORC project for the selected sessions")
|
|
184
|
+
parser.add_argument("--project-map", type=pathlib.Path, help="JSON mapping of source cwd prefixes or repository URLs to ORC project names")
|
|
185
|
+
parser.add_argument("--agent", choices=["all", "claude", "codex"], default="all")
|
|
186
|
+
parser.add_argument("--cwd", help="Only sessions at or below this local project directory")
|
|
187
|
+
parser.add_argument("--exclude", action="append", default=[], help="Skip native paths matching this glob; repeatable")
|
|
188
|
+
parser.add_argument("--root", type=pathlib.Path, help="Override native projects/sessions root; requires --agent")
|
|
189
|
+
parser.add_argument("--session-file", action="append", type=pathlib.Path, help="Explicit native file; repeatable; requires --agent")
|
|
190
|
+
parser.add_argument("--source-id", help="Stable origin ID; defaults to a UUID persisted in ~/.orc/session-sync-id")
|
|
191
|
+
parser.add_argument("--workers", type=int, choices=range(1, 5), default=2)
|
|
192
|
+
parser.add_argument("--reindex", action="store_true", help="Resend unchanged snapshots to refresh/recover their search index")
|
|
193
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview discovery and project mapping without uploads")
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
if args.grant_file and args.grant_stdin:
|
|
196
|
+
parser.error("Choose --grant-file or --grant-stdin")
|
|
197
|
+
if args.grant_file or args.grant_stdin:
|
|
198
|
+
grant = json.loads(args.grant_file.read_text()) if args.grant_file else json.load(sys.stdin)
|
|
199
|
+
args.url = grant.get("sync_url")
|
|
200
|
+
if not args.url:
|
|
201
|
+
parser.error("Set --url or ORC_API_BASE")
|
|
202
|
+
url = urllib.parse.urlparse(args.url)
|
|
203
|
+
if (args.grant_file or args.grant_stdin) and not re.search(r"/api/session-sync/[A-Za-z0-9_-]{43}$", url.path):
|
|
204
|
+
parser.error("Expected a scoped session_sync capability")
|
|
205
|
+
if url.username or url.password or url.query or url.fragment or url.scheme not in ("http", "https") or (url.scheme != "https" and url.hostname not in ("localhost", "127.0.0.1", "::1")):
|
|
206
|
+
parser.error("Use credential-free HTTPS (or loopback HTTP)")
|
|
207
|
+
if (args.root or args.session_file) and args.agent == "all":
|
|
208
|
+
parser.error("Select --agent with --root or --session-file")
|
|
209
|
+
base = args.url.rstrip("/") if re.search(r"/api/session-sync/[A-Za-z0-9_-]{43}$", url.path) else args.url.rstrip("/").removesuffix("/api") + "/api"
|
|
210
|
+
projects = request(base, "/projects?limit=100")["projects"]
|
|
211
|
+
names = {p["name"].casefold(): p["name"] for p in projects}
|
|
212
|
+
mappings = json.loads(args.project_map.read_text()) if args.project_map else {}
|
|
213
|
+
if not isinstance(mappings, dict) or any(not isinstance(k, str) or not isinstance(v, str) for k,v in mappings.items()):
|
|
214
|
+
parser.error("--project-map must map cwd/repository strings to project name strings")
|
|
215
|
+
if args.project and args.project.casefold() not in names:
|
|
216
|
+
parser.error("--project must name an existing ORC project")
|
|
217
|
+
identity = pathlib.Path.home() / ".orc" / "session-sync-id"
|
|
218
|
+
if args.source_id:
|
|
219
|
+
source_id = args.source_id
|
|
220
|
+
else:
|
|
221
|
+
identity.parent.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
try:
|
|
223
|
+
with identity.open("x") as handle:
|
|
224
|
+
handle.write(str(uuid.uuid4()))
|
|
225
|
+
except FileExistsError:
|
|
226
|
+
pass
|
|
227
|
+
source_id = identity.read_text().strip()
|
|
228
|
+
receipt_base = base.split("/api/session-sync/")[0] + "/api" if "/api/session-sync/" in base else base
|
|
229
|
+
receipts = pathlib.Path.home() / ".orc" / "session-sync-receipts" / hashlib.sha256(receipt_base.encode()).hexdigest()
|
|
230
|
+
if not args.dry_run:
|
|
231
|
+
receipts.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
candidates = []
|
|
233
|
+
for agent in (["claude", "codex"] if args.agent == "all" else [args.agent]):
|
|
234
|
+
root = args.root or ((pathlib.Path(os.environ.get("CLAUDE_CONFIG_DIR", str(pathlib.Path.home() / ".claude"))) / "projects") if agent == "claude" else pathlib.Path(os.environ.get("CODEX_HOME", str(pathlib.Path.home() / ".codex"))) / "sessions")
|
|
235
|
+
paths = args.session_file or sorted(root.rglob("*.jsonl"))
|
|
236
|
+
if not args.session_file and not args.root and agent == "codex":
|
|
237
|
+
paths += sorted((root.parent / "archived_sessions").rglob("*.jsonl"))
|
|
238
|
+
candidates.extend((agent, p) for p in paths if not p.is_symlink() and "subagents" not in p.parts and not any(p.match(pattern) for pattern in args.exclude))
|
|
239
|
+
def sync(item):
|
|
240
|
+
agent, path = item
|
|
241
|
+
try:
|
|
242
|
+
text, rows = snapshot(path)
|
|
243
|
+
upload = identify(agent, path, text, rows)
|
|
244
|
+
cwd = upload["metadata"].get("cwd", "")
|
|
245
|
+
if args.cwd and not (normalized_path(cwd) == normalized_path(os.path.abspath(args.cwd)) or normalized_path(cwd).startswith(normalized_path(os.path.abspath(args.cwd)) + "/")):
|
|
246
|
+
return "excluded"
|
|
247
|
+
project = args.project
|
|
248
|
+
if not project:
|
|
249
|
+
repo = repository_key(upload["metadata"].get("repository", ""))
|
|
250
|
+
matches = {value for key, value in mappings.items() if (cwd and (normalized_path(cwd) == normalized_path(key) or normalized_path(cwd).startswith(normalized_path(key) + "/"))) or (repo and repository_key(key) == repo)}
|
|
251
|
+
matches.update(p["name"] for p in projects if repo and p.get("git_remote_url") and repository_key(p["git_remote_url"]) == repo)
|
|
252
|
+
if len(matches) == 1:
|
|
253
|
+
project = matches.pop()
|
|
254
|
+
if not project or project.casefold() not in names:
|
|
255
|
+
print(f"UNMAPPED {agent} {upload['session_id']}; select --project or --project-map", file=sys.stderr)
|
|
256
|
+
return "unmapped"
|
|
257
|
+
upload.update(project=names[project.casefold()], source_id=source_id)
|
|
258
|
+
if args.reindex:
|
|
259
|
+
upload["reindex"] = True
|
|
260
|
+
if args.dry_run:
|
|
261
|
+
print(f"WOULD UPLOAD {agent} {upload['session_id']} -> {upload['project']}")
|
|
262
|
+
return "preview"
|
|
263
|
+
receipt_path = receipts / (hashlib.sha256(f"{source_id}:{agent}:{upload['session_id']}".encode()).hexdigest() + ".json")
|
|
264
|
+
local_hash = hashlib.sha256(json.dumps(upload["bundle"], sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
265
|
+
identity_request = {key: upload[key] for key in ("project", "source_id", "agent", "session_id")}
|
|
266
|
+
head = request(base, "/sessions/archive/manifest", {"sessions": [identity_request]})["sessions"][0]["archive"]
|
|
267
|
+
try:
|
|
268
|
+
receipt = json.loads(receipt_path.read_text())
|
|
269
|
+
except (OSError, ValueError):
|
|
270
|
+
receipt = {}
|
|
271
|
+
if not args.reindex and receipt.get("local_hash") == local_hash and head and receipt.get("remote_hash") == head["content_hash"]:
|
|
272
|
+
print(f"UNCHANGED {agent} {upload['session_id']} (manifest verified)")
|
|
273
|
+
return "unchanged"
|
|
274
|
+
result = request(base, "/sessions/archive/upload", upload)
|
|
275
|
+
outcome = result["outcome"]
|
|
276
|
+
if outcome in ("created", "updated", "unchanged"):
|
|
277
|
+
temporary = receipt_path.with_suffix("." + uuid.uuid4().hex + ".tmp")
|
|
278
|
+
temporary.write_text(json.dumps({"local_hash": local_hash, "remote_hash": result["session"]["content_hash"]}))
|
|
279
|
+
os.replace(temporary, receipt_path)
|
|
280
|
+
print(f"{outcome.upper()} {agent} {upload['session_id']} revision={result['session']['revision']}")
|
|
281
|
+
return outcome
|
|
282
|
+
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
283
|
+
print(f"FAILED {agent} {path.name}: {error}", file=sys.stderr)
|
|
284
|
+
return "failed"
|
|
285
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool:
|
|
286
|
+
outcomes = list(pool.map(sync, candidates))
|
|
287
|
+
counts = {key: outcomes.count(key) for key in sorted(set(outcomes))}
|
|
288
|
+
print(json.dumps({"discovered": len(candidates), "results": counts}))
|
|
289
|
+
return 1 if any(k in counts for k in ("failed", "conflict", "unmapped")) else 0
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
if __name__ == "__main__":
|
|
293
|
+
try:
|
|
294
|
+
sys.exit(main())
|
|
295
|
+
except (OSError, ValueError, KeyError) as error:
|
|
296
|
+
print(f"Session sync failed: {error}", file=sys.stderr)
|
|
297
|
+
sys.exit(1)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{r as b,j as e,U as J,W as X,X as Z,Y as M,Z as B,B as k,k as I,_ as ee,$ as K,a as te,u as U,a0 as ae,b as T,E as G,V as ne,L as se,a1 as re,a2 as ie,a3 as oe,a4 as de,a5 as le,a6 as ce,a7 as ue,a8 as W,d as D}from"./index-
|
|
1
|
+
import{r as b,j as e,U as J,W as X,X as Z,Y as M,Z as B,B as k,k as I,_ as ee,$ as K,a as te,u as U,a0 as ae,b as T,E as G,V as ne,L as se,a1 as re,a2 as ie,a3 as oe,a4 as de,a5 as le,a6 as ce,a7 as ue,a8 as W,d as D}from"./index-BqQDBYwx.js";import{p as ge,i as q}from"./public-api-BvD4z7ja.js";const _={specialist:`---
|
|
2
2
|
name: Specialist
|
|
3
3
|
description: Describe when to use this specialist
|
|
4
4
|
---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aa as T,j as e,a2 as L,a3 as P,a4 as $,a5 as R,a6 as O,E as w,S,ab as A,ac as h,r as k,ad as I,f as V,l as H,$ as z,a0 as B,ae as U,af as F,T as J,s as G,t as v,v as c,w as M,x,V as Y}from"./index-
|
|
1
|
+
import{aa as T,j as e,a2 as L,a3 as P,a4 as $,a5 as R,a6 as O,E as w,S,ab as A,ac as h,r as k,ad as I,f as V,l as H,$ as z,a0 as B,ae as U,af as F,T as J,s as G,t as v,v as c,w as M,x,V as Y}from"./index-BqQDBYwx.js";import{P as Z}from"./Pager-urh4-P2a.js";import{S as y}from"./scroll-area-YSA2ORI8.js";import{u as q}from"./useProjectScope-BvcpSOql.js";const K={file:"text-outline",task:"text-outline",decision:"text-outline",error:"text-error",git:"text-outline",rule:"text-outline",plan:"text-outline"},Q="text-outline";function W(n){return K[n]??Q}function X({sessionId:n,open:a,onClose:r}){const{data:t,isLoading:u,error:p,refetch:j}=T(n??"");return e.jsx(L,{open:a,onOpenChange:o=>!o&&r(),children:e.jsxs(P,{side:"right",className:"w-[540px] max-w-[90vw]",children:[e.jsx($,{children:e.jsx(R,{children:u?"Loading...":t?`${t.agent??"Session"} ${t.agent_version?`v${t.agent_version}`:""}`:"Session Detail"})}),e.jsxs(O,{children:[e.jsx("p",{className:"mb-4 text-sm text-outline",children:"This audit record contains summaries and events. Open Sessions to read native agent conversations or resume them."}),p?e.jsx(w,{message:p.message,onRetry:()=>j()}):u?e.jsx("div",{className:"space-y-4",children:[...Array(4)].map((o,i)=>e.jsx(S,{className:"h-8 w-full bg-surface-highest"},i))}):t?e.jsxs("div",{className:"space-y-6",children:[t.summary&&e.jsx("div",{className:"font-body text-xs text-on-surface leading-relaxed",children:e.jsx(A,{children:t.summary})}),e.jsxs("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-4",children:[e.jsx(h,{label:"ID",children:e.jsx("span",{className:"font-mono text-xs break-all",children:t.id})}),e.jsx(h,{label:"Project ID",children:t.project_id??"—"}),e.jsx(h,{label:"Job Run ID",children:t.job_run_id??"—"}),e.jsx(h,{label:"Tokens Used",children:t.tokens_used!=null?t.tokens_used.toLocaleString():"—"}),e.jsx(h,{label:"Created",children:new Date(t.created_at).toLocaleString()})]}),t.events.length>0&&e.jsxs("div",{children:[e.jsxs("div",{className:"font-label text-xs font-medium text-outline mb-3",children:["Events (",t.events.length,")"]}),e.jsx(y,{className:"h-[300px]",children:e.jsx("div",{className:"space-y-1",children:[...t.events].sort((o,i)=>new Date(o.created_at).getTime()-new Date(i.created_at).getTime()).map(o=>e.jsx(ee,{event:o},o.id))})})]}),t.snapshot&&e.jsxs("div",{children:[e.jsx("div",{className:"font-label text-xs font-medium text-outline mb-3",children:"Snapshot"}),e.jsx(y,{className:"h-[200px]",children:e.jsx("pre",{className:"font-mono text-xs text-on-surface-variant whitespace-pre-wrap break-words bg-background border-l border-border p-3",children:t.snapshot})})]})]}):e.jsx("div",{className:"font-body text-xs text-outline py-8 text-center",children:"Session not found"})]})]})})}function ee({event:n}){const[a,r]=k.useState(!1),t=n.data!=null;return e.jsxs("div",{className:"border-b border-border/65",children:[e.jsxs("button",{type:"button",className:"w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-surface-high transition-colors",onClick:()=>t&&r(!a),"aria-expanded":t?a:void 0,children:[t?a?e.jsx(I,{size:10,className:"text-outline flex-shrink-0"}):e.jsx(V,{size:10,className:"text-outline flex-shrink-0"}):e.jsx("span",{className:"w-[10px] flex-shrink-0"}),e.jsx("span",{className:`inline-flex items-center px-1.5 py-0.5 text-[10px] font-label font-medium ${W(n.type)}`,children:n.type}),e.jsx("span",{className:"flex-1 font-body text-xs text-outline truncate",children:se(n.data)}),e.jsx("span",{className:"font-label text-[10px] text-outline flex-shrink-0",children:new Date(n.created_at).toLocaleTimeString()})]}),a&&t&&e.jsx("div",{className:"px-3 pb-2",children:e.jsx("pre",{className:"font-mono text-xs text-on-surface-variant whitespace-pre-wrap break-words bg-background border-l border-border/65 p-2 max-h-48 overflow-auto",children:JSON.stringify(n.data,null,2)})})]})}function se(n){if(n==null)return"";if(typeof n=="string")return n;if(typeof n=="object"){const a=n;if(typeof a.path=="string")return a.path;if(typeof a.content=="string")return a.content.length>80?`${a.content.slice(0,80)}...`:a.content;if(typeof a.message=="string")return a.message;if(typeof a.summary=="string")return a.summary;const r=Object.keys(a);return r.length<=3?r.join(", "):`${r.length} fields`}return String(n)}const b=25,m="font-label text-xs font-medium text-outline";function te({projectId:n}){const a=H("sm"),[r,t]=z("order","desc",["asc","desc"]),[u,p]=k.useState({order:r,page:0}),j=u.order===r?u.page:0,o=s=>p({order:r,page:s}),{selectedId:i,openDetail:g,closeDetail:_}=B("/audit","sessionId"),{data:l,isLoading:D,error:N,refetch:E}=U({...n==="all"?{}:{project_id:n},limit:b,offset:j*b,order:r});if(N)return e.jsx(w,{message:N.message,onRetry:()=>E()});const f=(l==null?void 0:l.sessions)??[],C=(l==null?void 0:l.total)??0;return e.jsxs("section",{"data-testid":"logged-sessions",children:[e.jsx("div",{className:"mb-4 flex justify-end",children:e.jsxs("select",{"aria-label":"Sort audit by date","data-testid":"audit-date-sort",value:r,onChange:s=>{o(0),t(s.target.value)},className:"min-h-11 rounded-md border border-control-border bg-background px-3 text-sm text-on-surface",children:[e.jsx("option",{value:"desc",children:"Created: newest first"}),e.jsx("option",{value:"asc",children:"Created: oldest first"})]})}),D?e.jsx("div",{className:"space-y-2",children:[...Array(5)].map((s,d)=>e.jsx(S,{className:"h-10 w-full bg-surface-highest"},d))}):f.length===0?e.jsx(F,{message:"No audit records"}):e.jsxs("div",{className:"min-w-0 overflow-hidden",children:[a?e.jsxs(J,{children:[e.jsx(G,{children:e.jsxs(v,{className:"border-b border-border hover:bg-transparent",children:[e.jsx(c,{className:m,children:"Agent"}),e.jsx(c,{className:`${m} w-24`,children:"Version"}),e.jsx(c,{className:m,children:"Summary"}),e.jsx(c,{className:`${m} w-24`,children:"Project"}),e.jsx(c,{className:`${m} w-28`,children:"Tokens Used"}),e.jsx(c,{className:`${m} w-36`,children:"Created"})]})}),e.jsx(M,{children:f.map(s=>e.jsxs(v,{"data-testid":"session-row","data-session-id":s.id,className:"border-b border-border/65 hover:bg-surface-high cursor-pointer",onClick:()=>g(s.id),children:[e.jsx(x,{className:"font-label text-xs text-primary",children:e.jsx("button",{type:"button",className:"text-left font-medium hover:underline",onClick:d=>{d.stopPropagation(),g(s.id)},children:s.agent??"Session"})}),e.jsx(x,{className:"font-label text-xs text-outline",children:s.agent_version??"—"}),e.jsx(x,{className:"font-body text-xs text-on-surface-variant max-w-sm truncate",children:s.summary??"—"}),e.jsx(x,{className:"font-label text-xs text-outline",children:s.project_id?s.project_id.slice(-6):"—"}),e.jsx(x,{className:"font-label text-xs text-outline text-right",children:s.tokens_used!=null?s.tokens_used.toLocaleString():"—"}),e.jsx(x,{className:"font-label text-xs text-outline",children:new Date(s.created_at).toLocaleString()})]},s.id))})]}):e.jsx("div",{className:"divide-y divide-border",children:f.map(s=>{var d;return e.jsxs("article",{"data-testid":"session-row","data-session-id":s.id,className:"py-4 space-y-3",children:[e.jsx("button",{type:"button",className:"min-h-11 text-sm text-left font-medium text-primary break-words",onClick:()=>g(s.id),children:s.agent??"Session"}),e.jsx("p",{className:"text-sm leading-relaxed text-on-surface break-words",children:s.summary??"No summary recorded"}),e.jsxs("div",{className:"flex flex-wrap gap-x-4 gap-y-2 text-xs text-outline",children:[e.jsxs("span",{children:["Version ",s.agent_version??"—"]}),e.jsxs("span",{children:[((d=s.tokens_used)==null?void 0:d.toLocaleString())??"—"," tokens"]}),e.jsx("span",{children:s.project_id?`Project ${s.project_id.slice(-6)}`:"Unassigned"}),e.jsx("span",{children:new Date(s.created_at).toLocaleString()})]})]},s.id)})}),e.jsx(Z,{page:j,pageSize:b,total:C,onPage:o})]}),e.jsx(X,{sessionId:i,open:!!i,onClose:_})]})}function ie({projectId:n}){const a=q(n);return e.jsxs("div",{children:[e.jsx(Y,{title:"Audit"}),e.jsx("p",{className:"mb-4 text-sm text-outline",children:"Recorded summaries and events. Open Sessions to read conversations or resume a coding agent."}),e.jsx(te,{projectId:a},a)]})}export{ie as default};
|