teamshare-bridge 0.1.0 → 0.2.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/dist/bridge/daemon.js +239 -8
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/index.js +10 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/local-server.d.ts +6 -1
- package/dist/bridge/local-server.js +24 -0
- package/dist/bridge/local-server.js.map +1 -1
- package/dist/bridge/protocol.js +33 -4
- package/dist/bridge/protocol.js.map +1 -1
- package/dist/bridge/spawn.d.ts +20 -4
- package/dist/bridge/spawn.js +93 -29
- package/dist/bridge/spawn.js.map +1 -1
- package/dist/cli/index.js +1102 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/api.d.ts +203 -5
- package/dist/lib/api.js +144 -3
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/chat-reply.d.ts +22 -2
- package/dist/lib/chat-reply.js +139 -67
- package/dist/lib/chat-reply.js.map +1 -1
- package/dist/lib/config.d.ts +103 -0
- package/dist/lib/config.js +272 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/doc-reply.d.ts +11 -0
- package/dist/lib/doc-reply.js +18 -34
- package/dist/lib/doc-reply.js.map +1 -1
- package/dist/lib/harness.d.ts +38 -0
- package/dist/lib/harness.js +139 -0
- package/dist/lib/harness.js.map +1 -0
- package/dist/lib/llm.d.ts +30 -2
- package/dist/lib/llm.js +284 -30
- package/dist/lib/llm.js.map +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/models.d.ts +11 -0
- package/dist/lib/models.js +30 -1
- package/dist/lib/models.js.map +1 -1
- package/dist/lib/orchestrator/client.d.ts +27 -0
- package/dist/lib/orchestrator/client.js +135 -0
- package/dist/lib/orchestrator/client.js.map +1 -0
- package/dist/lib/orchestrator/index.d.ts +5 -0
- package/dist/lib/orchestrator/index.js +17 -0
- package/dist/lib/orchestrator/index.js.map +1 -0
- package/dist/lib/orchestrator/runner.d.ts +18 -0
- package/dist/lib/orchestrator/runner.js +177 -0
- package/dist/lib/orchestrator/runner.js.map +1 -0
- package/dist/lib/orchestrator/worktree.d.ts +43 -0
- package/dist/lib/orchestrator/worktree.js +243 -0
- package/dist/lib/orchestrator/worktree.js.map +1 -0
- package/dist/lib/session-stream.d.ts +1 -1
- package/dist/lib/skills.d.ts +21 -0
- package/dist/lib/skills.js +80 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/usage.d.ts +52 -0
- package/dist/lib/usage.js +69 -0
- package/dist/lib/usage.js.map +1 -0
- package/package.json +3 -1
- package/skills/teamshare-auth-rbac/SKILL.md +79 -0
- package/skills/teamshare-cdd/SKILL.md +145 -0
- package/skills/teamshare-modularity/SKILL.md +92 -0
- package/tools/usage_report.py +223 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: teamshare-cdd
|
|
3
|
+
description: Documents-driven development for TeamShare. Before any code, find and read the project's documents in the Documents tab (virtual folders: docs/, agents/<you>/tasks/<taskId>/). Write plan.md BEFORE starting, keep progress.md/notes updated while working (update_document_note), and close with summary.md. Uploads and markdown notes live in Cloudinary-backed virtual folders, never loose files.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TeamShare Documents-Driven Development (CDD)
|
|
7
|
+
|
|
8
|
+
You are working a task in a TeamShare project. In TeamShare, **the project's
|
|
9
|
+
Documents tab is the source of truth** — the same way `docs/` + `AGENTS.md`
|
|
10
|
+
drive TeamShare's own development at the repo root. Documents are organized
|
|
11
|
+
in **virtual folders** (a lowercase path stored on each document; the UI
|
|
12
|
+
renders them as a tree). Your job is to work from the docs, write the docs,
|
|
13
|
+
and leave the task better documented than you found it.
|
|
14
|
+
|
|
15
|
+
## The document library convention
|
|
16
|
+
|
|
17
|
+
Every project document carries a `folderPath` (lowercase kebab segments, `/`
|
|
18
|
+
= root, max 8 levels). The folders an agent creates MUST follow this layout:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
docs/ <- shared, project-wide knowledge
|
|
22
|
+
├── contracts/ <- API/endpoint contract notes
|
|
23
|
+
├── adr/ <- ADR-<n>-<title>.md architecture decisions
|
|
24
|
+
└── guides/ <- how-to guides for humans + agents
|
|
25
|
+
agents/
|
|
26
|
+
└── <agent-slug>/ <- ONE folder per agent (short, stable slug)
|
|
27
|
+
└── tasks/
|
|
28
|
+
└── <taskId>/ <- per-task workspace
|
|
29
|
+
├── plan.md <- written BEFORE starting the work
|
|
30
|
+
├── progress.md <- updated while working (or rely on subtasks)
|
|
31
|
+
└── summary.md <- written on completion
|
|
32
|
+
reports/ <- milestone reports (optional)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Rules that are not optional:
|
|
36
|
+
|
|
37
|
+
- **Kebab-case** document names ending in `.md` (`.markdown`/`.txt` ok).
|
|
38
|
+
- **Lowercase** folder paths; no spaces, no leading/trailing slashes.
|
|
39
|
+
- **One folder per agent, one subfolder per task.** Never write outside your
|
|
40
|
+
own folder except into `docs/**` when you add shared knowledge.
|
|
41
|
+
- Document names are unique enough to be obvious (`plan.md`, `progress.md`,
|
|
42
|
+
`summary.md`, `adr-001-auth-rbac.md`).
|
|
43
|
+
|
|
44
|
+
## The workflow — three gates
|
|
45
|
+
|
|
46
|
+
### START (before touching any code)
|
|
47
|
+
1. `list_document_folders` + `list_documents` for the project.
|
|
48
|
+
2. Read `docs/contracts`, `docs/adr`, `docs/guides` that exist — they govern
|
|
49
|
+
your implementation (same way `docs/api-contract.md` and
|
|
50
|
+
`docs/ui-style-guide.md` govern TeamShare's repos).
|
|
51
|
+
3. Read your own task folder if this is a `--continue`/resumed session.
|
|
52
|
+
4. **Write `plan.md` FIRST** via `create_document`: goal, decisions, files to
|
|
53
|
+
touch, step list. No code until the plan note exists.
|
|
54
|
+
5. **Move the task to `in_progress`** via `update_task` — you are now working it.
|
|
55
|
+
|
|
56
|
+
### DO (while working)
|
|
57
|
+
6. Check off subtasks (`update_subtask done: true`) and update
|
|
58
|
+
`progress.md`/`plan.md` via `update_document_note` as you go — the human
|
|
59
|
+
watches the app.
|
|
60
|
+
7. When you make an architecture decision worth keeping, add it to `docs/adr`
|
|
61
|
+
as `ADR-<n>-<title>.md` (short: context → decision → consequences).
|
|
62
|
+
8. Blocked? Use `ask_human` and include a pointer to the relevant note.
|
|
63
|
+
|
|
64
|
+
### DONE (before closing)
|
|
65
|
+
9. **Move the task to `in_review`** via `update_task` — signals the human
|
|
66
|
+
to review your work.
|
|
67
|
+
10. Write `summary.md`: what you built, why, how it maps to the plan, links to
|
|
68
|
+
related docs, follow-ups.
|
|
69
|
+
11. Post the closing comment referencing the summary doc.
|
|
70
|
+
12. Never leave a task without `plan.md` + `summary.md` notes.
|
|
71
|
+
|
|
72
|
+
## Task status progression (guidance)
|
|
73
|
+
|
|
74
|
+
Status can be set to **any value from any state** — the system allows free
|
|
75
|
+
transitions and reversal. No blocking errors. Use this as a guideline:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
open → in_progress → in_review → resolved → closed
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| When | Set status to | Why |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| You start working the task | `in_progress` | You claimed it, work has begun |
|
|
84
|
+
| Work is done, ready for human review | `in_review` | Human should review before you close |
|
|
85
|
+
| Human approves your work | `resolved` | Approved, ready to close |
|
|
86
|
+
| Final close (usually human) | `closed` | Task is fully complete |
|
|
87
|
+
|
|
88
|
+
**Tip:** Set status as you go. The human sees live progress in the app.
|
|
89
|
+
You can always reverse (e.g. `in_progress` → `open` if you need to reassess).
|
|
90
|
+
Don't jump to `resolved` or `closed` unless the human explicitly approves —
|
|
91
|
+
those are typically human-gated gates.
|
|
92
|
+
|
|
93
|
+
## Build mode (IMP-700) — the project's workflow engine
|
|
94
|
+
|
|
95
|
+
Some projects run **build mode**: the project has a build agent, and tasks
|
|
96
|
+
marked **ready for dev** (`readyForDev: true`) form an ordered build queue
|
|
97
|
+
(`sortOrder` asc, `createdAt` asc) that the agent works **strictly one at a
|
|
98
|
+
time, fully completing each**. When you are working a task as part of a build
|
|
99
|
+
run, the following rules apply ON TOP of everything above:
|
|
100
|
+
|
|
101
|
+
- **Ready for dev is the gate.** A task stays out of the build queue until a
|
|
102
|
+
human (or the planner) sets `readyForDev: true`. The queue only contains
|
|
103
|
+
`readyForDev=true AND status=open` tasks (plus blocked in-progress ones).
|
|
104
|
+
- **Strict order, full completion.** Work the queue in exact order — never
|
|
105
|
+
pick a later task while an earlier one is open. Do not move on until the
|
|
106
|
+
current task is genuinely complete: build passes, `plan.md` written first,
|
|
107
|
+
subtasks checked off, `summary.md` written, status moved to `in_review` or
|
|
108
|
+
`resolved`.
|
|
109
|
+
- **Failure protocol (never block the queue).** If a task cannot be finished,
|
|
110
|
+
the BUILD LOOP (not you) handles it: it leaves a `[question]` comment on the
|
|
111
|
+
task (marked with the literal prefix `[question]`) and a message in the
|
|
112
|
+
project's `#agent-questions` channel, then continues with the next ready
|
|
113
|
+
task. When a human answers, the loop resumes the skipped task automatically.
|
|
114
|
+
- **Planning (`--draft`).** When the human asks you to plan a build run from a
|
|
115
|
+
goal, create the tasks yourself with `create_task`:
|
|
116
|
+
- `readyForDev: true` so they enter the queue,
|
|
117
|
+
- increasing `sortOrder` (1, 2, 3, ...) so they are worked strictly in order,
|
|
118
|
+
- `assigneeId "me"` so the build agent owns them,
|
|
119
|
+
- dependencies first (a task that unblocks others comes before them),
|
|
120
|
+
- write the plan into `agents/<your-slug>/tasks/<taskId>/plan.md` per task,
|
|
121
|
+
and a shared `docs/guides/build-plan.md` note summarizing the order.
|
|
122
|
+
- **Progress signals.** Push the task to `in_progress` when you start it and
|
|
123
|
+
`in_review` when done — the build loop watches status to decide
|
|
124
|
+
done vs. failed, so a task left in `in_progress` at the end of a session is
|
|
125
|
+
treated as failed and skipped.
|
|
126
|
+
|
|
127
|
+
## Tools to use
|
|
128
|
+
|
|
129
|
+
- `list_document_folders <projectId>` — the tree, with per-folder counts.
|
|
130
|
+
- `list_documents <projectId>` — files (optionally filter by folder).
|
|
131
|
+
- `read_document <documentId>` — content (notes return their markdown body).
|
|
132
|
+
- `create_document` — write a note (`name`, `content`, `folderPath`).
|
|
133
|
+
- `update_document_note <documentId> <content>` — replace a note's body.
|
|
134
|
+
- Uploads (PDFs, images, binaries) are Cloudinary-hosted into
|
|
135
|
+
`documents/<folderPath>` and recorded as file documents — mirror the same
|
|
136
|
+
folder convention.
|
|
137
|
+
|
|
138
|
+
## References
|
|
139
|
+
|
|
140
|
+
- `docs/agent-tasks/agent-hub-connection.md` — the agent contract.
|
|
141
|
+
- `docs/api-contract.md` — response envelope, pagination, type mirroring.
|
|
142
|
+
- TeamShare root `docs/` + `IMPLEMENTATION_TRACKER.md` — the convention this
|
|
143
|
+
skill mirrors.
|
|
144
|
+
- Backend: `GET /documents/folders`, `POST /documents/note`,
|
|
145
|
+
`PATCH /documents/:id/content`, `GET /documents/:id/raw`.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: teamshare-modularity
|
|
3
|
+
description: Enforce TeamShare's modular architecture when writing code in teamshare-backend, frontend, teamshare-mobile-app or teamshare-bridge. Follow the per-repo layout (src/modules/<feature>/{controller,service,dto,module}, src/components/{ui,shared}, modules/<feature>/{views,components,columns,hooks,utils}, src/lib/api per feature), never import vendor ui/ in app code, use TS* wrappers, zod-first DTOs, the response envelope, and pass the repo build/typecheck gates before done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TeamShare Modular Architecture Enforcement
|
|
7
|
+
|
|
8
|
+
TeamShare is four separate repos with strict internal layouts. Before writing
|
|
9
|
+
code, check which repo you are in and follow its module structure exactly.
|
|
10
|
+
These rules are enforced per repo — know where you are.
|
|
11
|
+
|
|
12
|
+
## 1. teamshare-backend (NestJS + Prisma 6 + zod)
|
|
13
|
+
|
|
14
|
+
- **Layout:** a feature lives in `src/modules/<plural-feature>/` with
|
|
15
|
+
`<feature>.controller.ts`, `<feature>.service.ts`, `<feature>.dto.ts`,
|
|
16
|
+
`<feature>.module.ts` (+ `*.spec.ts`). Cross-cutting infra goes in
|
|
17
|
+
`src/common/<area>/` (constants, decorators, dto, filters, guards,
|
|
18
|
+
interceptors, pipes, prisma, utils, ...) — never inside a feature module.
|
|
19
|
+
- **DTOs are zod-first:** `export const CreateXSchema = z.object({...})` +
|
|
20
|
+
`export type CreateXDto = z.infer<typeof ...>`. Validate via
|
|
21
|
+
`@Body(new ZodValidationPipe(Schema))`. Import enum schemas from
|
|
22
|
+
`src/generated/zod/schemas/enums/...`; never hand-edit `src/generated/zod`.
|
|
23
|
+
- **Every endpoint returns the envelope** `{ success, data, pagination? }`
|
|
24
|
+
(TransformInterceptor) — handlers return raw data, lists return
|
|
25
|
+
`{ items, pagination }` via `computePagination`.
|
|
26
|
+
- **Every endpoint declares access metadata** (`@Permissions`, or
|
|
27
|
+
`@Public()` + `@UseGuards(AgentsDualAuthGuard)` + `@ApiKeyScopes` for the
|
|
28
|
+
agent+human surface) — see the teamshare-auth-rbac skill.
|
|
29
|
+
- **Prisma:** `prisma/schema.prisma` is the single source of truth. After a
|
|
30
|
+
schema change run `npx prisma generate` (do not hand-edit generated zod).
|
|
31
|
+
Deploy migrations with `npx prisma migrate deploy`, never `migrate dev`.
|
|
32
|
+
- **Error style:** throw exceptions with a machine-readable `code`
|
|
33
|
+
(`new BadRequestException({ code: 'X', message: '...' })`).
|
|
34
|
+
|
|
35
|
+
## 2. frontend (Next.js App Router + Tailwind v4 + TanStack Query)
|
|
36
|
+
|
|
37
|
+
- **Two component layers, never mixed:**
|
|
38
|
+
- `src/components/ui/` = VENDOR shadcn primitives — do not edit, do not
|
|
39
|
+
import from app code.
|
|
40
|
+
- `src/components/shared/` = TeamShare `TS*` wrappers. **App code imports
|
|
41
|
+
ONLY from `@/components/shared`** (barrel `index.ts`). Build new shared
|
|
42
|
+
components here, named `TS*`, with `data-slot="ts-*"` and `--ts-*` tokens.
|
|
43
|
+
- **Feature modules:** `src/modules/<feature>/{views,components,columns,hooks,utils}`.
|
|
44
|
+
Pages under `src/app/` stay thin and delegate to views. Route tabs use
|
|
45
|
+
`useUrlTab` (`?tab=...`).
|
|
46
|
+
- **API + data:** per-feature `src/lib/api/<feature>.ts` exporting a
|
|
47
|
+
`<feature>Api` object of `listX/getX/createX/updateX/deleteX`; envelope-aware
|
|
48
|
+
`apiFetch` from `@/lib/api/client`. Query keys via `qk.*` factory
|
|
49
|
+
(`src/lib/query-keys.ts`), list state via `useListState`.
|
|
50
|
+
- **Validation mirror:** zod schemas live in `src/lib/validation/schemas.ts`
|
|
51
|
+
and MIRROR the backend contract (backend is canonical). Enums derive from
|
|
52
|
+
the schemas in `src/lib/constants/enums.ts`.
|
|
53
|
+
- **Tables/forms:** TanStack React Table **v8** inside `TSTable`; forms always
|
|
54
|
+
`TSForm` + `TSFormField` + `zodResolver`.
|
|
55
|
+
- **IconSax hard rule:** `iconsax-react`, every icon passes `variant` AND
|
|
56
|
+
`color`; pick names from `docs/ui-style-guide.md` §6.4. No emoji as icons.
|
|
57
|
+
|
|
58
|
+
## 3. teamshare-mobile-app (Expo SDK 57 + NativeWind v4)
|
|
59
|
+
|
|
60
|
+
Same two-layer rule (`ui/` vendor vs `shared/` `TS*`), same zod schema
|
|
61
|
+
mirroring, same `modules/<feature>/` organization. Themexing via `--ts-*`
|
|
62
|
+
tokens; run `npx expo start --clear` when classes misbehave on Windows.
|
|
63
|
+
|
|
64
|
+
## 4. teamshare-bridge (plain CommonJS TypeScript)
|
|
65
|
+
|
|
66
|
+
- `src/lib/` = shared modules (plain exported functions + classes, named
|
|
67
|
+
exports, single quotes, JSDoc header naming the phase/IMP). `src/cli/` and
|
|
68
|
+
`src/bridge/` hold the two binaries' entry points. Custom `parseArgs`, no
|
|
69
|
+
arg libraries.
|
|
70
|
+
- **Best-effort by design:** heartbeats, closing comments, streaming, config
|
|
71
|
+
writes and lock release never throw; log with `console.warn`/empty catch.
|
|
72
|
+
Only the API client propagates errors (`ApiError`), with `msg()` for logs.
|
|
73
|
+
- Locks (`~/.teamshare/locks/<agentId>.lock`) gate every session; use
|
|
74
|
+
`acquireLock`/`releaseLock`; never bypass the wake queue.
|
|
75
|
+
- **Gates:** `npm run build` and `npm run typecheck` — `npm run lint` is
|
|
76
|
+
broken (eslint is not installed), do not rely on it.
|
|
77
|
+
|
|
78
|
+
## Cross-repo rules
|
|
79
|
+
|
|
80
|
+
- Backend schema is canonical; mirrors are for UX. A stale mirror is a bug.
|
|
81
|
+
- No new dependencies without a reason; prefer Node built-ins / existing libs.
|
|
82
|
+
- Commit per repo, never cross-commit.
|
|
83
|
+
|
|
84
|
+
## Pre-done checklist (all must pass)
|
|
85
|
+
|
|
86
|
+
- [ ] Backend: `npm run build` green; eslint clean on the files you touched.
|
|
87
|
+
- [ ] Backend: zod-first DTO + `ZodValidationPipe` on every new endpoint.
|
|
88
|
+
- [ ] Backend: envelope + pagination + access metadata on every endpoint.
|
|
89
|
+
- [ ] Frontend/mobile: imports only from `@/components/shared`; `TS*` used.
|
|
90
|
+
- [ ] Frontend/mobile: `npm run build` (web) / `npx expo export` (mobile) green.
|
|
91
|
+
- [ ] Bridge: `npm run build` + `npm run typecheck` green.
|
|
92
|
+
- [ ] No generated files hand-edited; no secrets committed.
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
usage_report.py - read-only agent spend report from the local opencode db.
|
|
4
|
+
|
|
5
|
+
Queries ~/.local/share/opencode/opencode.db (SQLite, WAL mode, ~3.5 GB) with a
|
|
6
|
+
READ-ONLY uri - never writes, never copies. Node has no built-in sqlite, so
|
|
7
|
+
the teamshare-agent CLI shells out to this script (offline, no deps, stdlib
|
|
8
|
+
only: sqlite3/argparse/json/datetime).
|
|
9
|
+
|
|
10
|
+
Attribution rules (source of truth: the `session` table):
|
|
11
|
+
- title starting "TeamShare " -> TeamShare bridge agent session
|
|
12
|
+
- providerID == "opencode" -> the user's own opencode windows
|
|
13
|
+
- everything else -> other agent work (opencode-go, ...)
|
|
14
|
+
|
|
15
|
+
Baseline (2026-08-16): $1.0445 across 14 cost-bearing sessions.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python usage_report.py [--day YYYY-MM-DD] [--agent <id>] [--json]
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import datetime
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sqlite3
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
DB_PATH = os.path.join(
|
|
28
|
+
os.path.expanduser("~"), ".local", "share", "opencode", "opencode.db"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def db_uri() -> str:
|
|
33
|
+
"""Read-only sqlite uri - NEVER open opencode.db for write (corruption
|
|
34
|
+
while opencode runs); NEVER copy the 3.5 GB file. Query in place."""
|
|
35
|
+
return "file:" + DB_PATH.replace("\\", "/") + "?mode=ro"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def day_bounds(day: str) -> tuple[int, int]:
|
|
39
|
+
"""Local-midnight ms-epoch bounds for a YYYY-MM-DD day."""
|
|
40
|
+
d = datetime.datetime.strptime(day, "%Y-%m-%d")
|
|
41
|
+
start = d.replace(tzinfo=datetime.datetime.now().astimezone().tzinfo)
|
|
42
|
+
end = start + datetime.timedelta(days=1)
|
|
43
|
+
return int(start.timestamp() * 1000), int(end.timestamp() * 1000)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_model(model_json: str | None) -> dict:
|
|
47
|
+
"""`model` is a JSON string like {"id":..., "providerID":...}."""
|
|
48
|
+
if not model_json:
|
|
49
|
+
return {}
|
|
50
|
+
try:
|
|
51
|
+
parsed = json.loads(model_json)
|
|
52
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
53
|
+
except (json.JSONDecodeError, TypeError):
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def attribution(title: str | None, provider_id: str | None) -> tuple[str, bool]:
|
|
58
|
+
"""(label, is_teamshare): 'user' for the user's own windows, 'agent'
|
|
59
|
+
otherwise; TeamShare-titled sessions are flagged for the bridge split."""
|
|
60
|
+
if title and title.startswith("TeamShare "):
|
|
61
|
+
return "agent", True
|
|
62
|
+
if provider_id == "opencode":
|
|
63
|
+
return "user", False
|
|
64
|
+
return "agent", False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def fmt_duration(ms: int) -> str:
|
|
68
|
+
seconds = max(0, ms) // 1000
|
|
69
|
+
if seconds < 60:
|
|
70
|
+
return f"{seconds}s"
|
|
71
|
+
minutes, sec = divmod(seconds, 60)
|
|
72
|
+
return f"{minutes}m {sec}s" if minutes < 60 else f"{minutes // 60}h {minutes % 60}m"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def fmt_time(ms: int) -> str:
|
|
76
|
+
return datetime.datetime.fromtimestamp(ms / 1000).strftime("%H:%M")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run(day: str, agent: str | None, as_json: bool) -> int:
|
|
80
|
+
if not os.path.exists(DB_PATH):
|
|
81
|
+
print(
|
|
82
|
+
f"usage: opencode db not found at {DB_PATH} (has opencode ever run?)",
|
|
83
|
+
file=sys.stderr,
|
|
84
|
+
)
|
|
85
|
+
return 1
|
|
86
|
+
try:
|
|
87
|
+
con = sqlite3.connect(db_uri(), uri=True)
|
|
88
|
+
except sqlite3.Error as err:
|
|
89
|
+
print(f"usage: cannot open opencode db read-only: {err}", file=sys.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
start_ms, end_ms = day_bounds(day)
|
|
93
|
+
try:
|
|
94
|
+
rows = con.execute(
|
|
95
|
+
"SELECT title, model, cost, tokens_input, tokens_output,"
|
|
96
|
+
" tokens_reasoning, tokens_cache_read, tokens_cache_write,"
|
|
97
|
+
" time_created, time_updated"
|
|
98
|
+
" FROM session WHERE time_created >= ? AND time_created < ?"
|
|
99
|
+
" ORDER BY time_created",
|
|
100
|
+
(start_ms, end_ms),
|
|
101
|
+
).fetchall()
|
|
102
|
+
except sqlite3.Error as err:
|
|
103
|
+
print(f"usage: query failed: {err}", file=sys.stderr)
|
|
104
|
+
con.close()
|
|
105
|
+
return 1
|
|
106
|
+
con.close()
|
|
107
|
+
|
|
108
|
+
sessions = []
|
|
109
|
+
for (title, model_json, cost, t_in, t_out, t_reason, t_cr, t_cw,
|
|
110
|
+
created, updated) in rows:
|
|
111
|
+
cost = cost or 0.0
|
|
112
|
+
title = title or "(untitled)"
|
|
113
|
+
# Exclude opencode's "New session - ..." placeholders (empty shells
|
|
114
|
+
# that never carry a title; one even holds 547k cached tokens at
|
|
115
|
+
# $0). The baseline counts the remaining cost-bearing sessions, but
|
|
116
|
+
# REAL sessions on free models also matter for attribution - they
|
|
117
|
+
# show up at $0.0000 when they used tokens.
|
|
118
|
+
if title.startswith("New session"):
|
|
119
|
+
continue
|
|
120
|
+
if cost <= 0 and not (t_in or t_out or t_reason or t_cr or t_cw):
|
|
121
|
+
continue
|
|
122
|
+
model = parse_model(model_json)
|
|
123
|
+
provider_id = model.get("providerID") or ""
|
|
124
|
+
label, teamshare = attribution(title, provider_id)
|
|
125
|
+
sessions.append(
|
|
126
|
+
{
|
|
127
|
+
"title": title,
|
|
128
|
+
"model": model.get("id") or "",
|
|
129
|
+
"providerID": provider_id,
|
|
130
|
+
"cost": round(cost, 6),
|
|
131
|
+
"tokensInput": t_in or 0,
|
|
132
|
+
"tokensOutput": t_out or 0,
|
|
133
|
+
"tokensReasoning": t_reason or 0,
|
|
134
|
+
"tokensCacheRead": t_cr or 0,
|
|
135
|
+
"tokensCacheWrite": t_cw or 0,
|
|
136
|
+
"timeCreated": created,
|
|
137
|
+
"timeUpdated": updated,
|
|
138
|
+
"durationMin": round(max(0, (updated or created) - created) / 60000, 1),
|
|
139
|
+
"attribution": label,
|
|
140
|
+
"teamshare": teamshare,
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
total_cost = round(sum(s["cost"] for s in sessions), 6)
|
|
145
|
+
user_cost = round(sum(s["cost"] for s in sessions if s["attribution"] == "user"), 6)
|
|
146
|
+
agent_cost = round(total_cost - user_cost, 6)
|
|
147
|
+
teamshare_cost = round(sum(s["cost"] for s in sessions if s["teamshare"]), 6)
|
|
148
|
+
|
|
149
|
+
# --agent <id> filters to sessions whose title carries `agent <id>`
|
|
150
|
+
# (the format every bridge session now records: "TeamShare task <taskId>
|
|
151
|
+
# (agent <agentId>)").
|
|
152
|
+
if agent:
|
|
153
|
+
needle = f"agent {agent}".lower()
|
|
154
|
+
sessions = [s for s in sessions if needle in s["title"].lower()]
|
|
155
|
+
total_cost = round(sum(s["cost"] for s in sessions), 6)
|
|
156
|
+
user_cost = round(sum(s["cost"] for s in sessions if s["attribution"] == "user"), 6)
|
|
157
|
+
agent_cost = round(total_cost - user_cost, 6)
|
|
158
|
+
teamshare_cost = round(sum(s["cost"] for s in sessions if s["teamshare"]), 6)
|
|
159
|
+
|
|
160
|
+
tokens = {
|
|
161
|
+
"input": sum(s["tokensInput"] for s in sessions),
|
|
162
|
+
"output": sum(s["tokensOutput"] for s in sessions),
|
|
163
|
+
"reasoning": sum(s["tokensReasoning"] for s in sessions),
|
|
164
|
+
"cacheRead": sum(s["tokensCacheRead"] for s in sessions),
|
|
165
|
+
"cacheWrite": sum(s["tokensCacheWrite"] for s in sessions),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if as_json:
|
|
169
|
+
print(
|
|
170
|
+
json.dumps(
|
|
171
|
+
{
|
|
172
|
+
"day": day,
|
|
173
|
+
"dbPath": DB_PATH,
|
|
174
|
+
"totalCost": total_cost,
|
|
175
|
+
"sessionCount": len(sessions),
|
|
176
|
+
"userCost": user_cost,
|
|
177
|
+
"agentCost": agent_cost,
|
|
178
|
+
"teamshareCost": teamshare_cost,
|
|
179
|
+
"tokens": tokens,
|
|
180
|
+
"sessions": sessions,
|
|
181
|
+
},
|
|
182
|
+
indent=2,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
# Human table.
|
|
188
|
+
print(f"Agent spend report - {day} (local time)")
|
|
189
|
+
print(f"Source: {DB_PATH} (read-only)")
|
|
190
|
+
print(f"Total: ${total_cost:.4f} across {len(sessions)} sessions")
|
|
191
|
+
print(f" user windows : ${user_cost:.4f}")
|
|
192
|
+
print(f" agent sessions : ${agent_cost:.4f} (of which TeamShare ${teamshare_cost:.4f})")
|
|
193
|
+
print(f"Tokens: {tokens['input']:,} in / {tokens['output']:,} out / "
|
|
194
|
+
f"{tokens['reasoning']:,} reasoning / "
|
|
195
|
+
f"{tokens['cacheRead']:,} cache-read / {tokens['cacheWrite']:,} cache-write")
|
|
196
|
+
print()
|
|
197
|
+
print(f"{'time':<13} {'dur':<8} {'cost':<8} {'src':<8} {'model':<34} title")
|
|
198
|
+
print("-" * 110)
|
|
199
|
+
for s in sessions:
|
|
200
|
+
src = "user" if s["attribution"] == "user" else ("teamshare" if s["teamshare"] else "agent")
|
|
201
|
+
span = f"{fmt_time(s['timeCreated'])}-{fmt_time(s['timeUpdated'])}"
|
|
202
|
+
print(
|
|
203
|
+
f"{span:<13} {s['durationMin']:>4}m {s['cost']:>7.4f} {src:<8} "
|
|
204
|
+
f"{(s['providerID'] + '/' + s['model'])[:34]:<34} {s['title'][:60]}"
|
|
205
|
+
)
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main() -> int:
|
|
210
|
+
parser = argparse.ArgumentParser(description="Read-only opencode spend report")
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"--day",
|
|
213
|
+
default=datetime.date.today().isoformat(),
|
|
214
|
+
help="YYYY-MM-DD (default: today, local time)",
|
|
215
|
+
)
|
|
216
|
+
parser.add_argument("--agent", default=None, help="filter sessions for one agent id")
|
|
217
|
+
parser.add_argument("--json", action="store_true", help="machine-readable output")
|
|
218
|
+
args = parser.parse_args()
|
|
219
|
+
return run(args.day, args.agent, args.json)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if __name__ == "__main__":
|
|
223
|
+
sys.exit(main())
|