@codeoid/core 0.1.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 +39 -0
- package/package.json +28 -0
- package/src/approvals.ts +60 -0
- package/src/client.ts +626 -0
- package/src/format.ts +91 -0
- package/src/identity.ts +58 -0
- package/src/index.ts +9 -0
- package/src/messages.ts +304 -0
- package/src/resume.ts +74 -0
- package/src/sanitize-url.ts +86 -0
- package/src/slash.ts +247 -0
- package/src/usage-days.ts +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @codeoid/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic client core for the [Codeoid](https://github.com/saucam/codeoid)
|
|
4
|
+
daemon — everything a frontend needs except the pixels:
|
|
5
|
+
|
|
6
|
+
- **`CodeoidClient`** — the WebSocket transport: auth handshake (token +
|
|
7
|
+
protocol version + capability declaration), request/response correlation,
|
|
8
|
+
liveness heartbeat, exponential-backoff reconnect with full jitter. Needs
|
|
9
|
+
only a WHATWG `WebSocket` global (browsers, React Native, Bun, Node ≥ 22).
|
|
10
|
+
All timing is injectable. Native hosts call `reconnectNow()` from their own
|
|
11
|
+
resume signal (e.g. React Native `AppState`).
|
|
12
|
+
- **`MessageStore`** + kernels (`mergeDeltaInto`, `dedupeReplay`) — the
|
|
13
|
+
single source of truth for transcript accumulation: upsert-by-messageId,
|
|
14
|
+
streaming delta merges, snapshot vs chunked vs incremental replay routing
|
|
15
|
+
(`ingest()`), per-message versions and per-session epochs.
|
|
16
|
+
- **`ResumeCursors`** — `replay.resume` cursor tracking so reconnects fetch
|
|
17
|
+
only the tail mutated since, not the whole scrollback.
|
|
18
|
+
- Display helpers shared across frontends: usage formatters (`formatTokens`,
|
|
19
|
+
`formatCostUsd`, …), identity/provenance labels (`shortSub`,
|
|
20
|
+
`identityLabel`, …), approval scanning, slash-command parsing.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { CodeoidClient, MessageStore, ResumeCursors } from "@codeoid/core";
|
|
24
|
+
import { CAPABILITIES } from "@codeoid/protocol";
|
|
25
|
+
|
|
26
|
+
const store = new MessageStore();
|
|
27
|
+
const cursors = new ResumeCursors();
|
|
28
|
+
const client = new CodeoidClient({
|
|
29
|
+
url: "ws://localhost:7400",
|
|
30
|
+
token,
|
|
31
|
+
capabilities: [CAPABILITIES.PARTS, CAPABILITIES.CHUNKED_REPLAY, CAPABILITIES.SEQ_RESUME],
|
|
32
|
+
clientName: "my-frontend/1.0",
|
|
33
|
+
});
|
|
34
|
+
client.onMessage((msg) => store.ingest(msg, cursors));
|
|
35
|
+
await client.connect();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Ships TypeScript source (every consumer transpiles TS — Bun, Vite, Metro).
|
|
39
|
+
`@codeoid/protocol` is a peer dependency.
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeoid/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic Codeoid client core — WebSocket transport (auth handshake, reconnect, heartbeat), message store semantics, resume cursors, and display helpers. Shared by the web UI and the mobile client.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/saucam/codeoid.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"default": "./src/index.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"files": ["src", "!src/**/*.test.ts", "README.md"],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@codeoid/protocol": "^0.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/approvals.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared pending-approval lookup for ApprovalBar + desktop notifications.
|
|
3
|
+
*
|
|
4
|
+
* The naive version scanned the ENTIRE message array from index 0 on every
|
|
5
|
+
* streaming delta — O(N) per delta on a 5 000-message session. Two levers
|
|
6
|
+
* keep this cheap while preserving user-visible behavior:
|
|
7
|
+
*
|
|
8
|
+
* 1. Status gate. Approvals can only pend mid-turn, so when the session
|
|
9
|
+
* is `idle` / `error` there is nothing to find and we skip the scan
|
|
10
|
+
* entirely. We deliberately gate on "any active status" rather than
|
|
11
|
+
* strictly `waiting_approval`: with parallel pending approvals (rare,
|
|
12
|
+
* stream-input mode) the daemon flips status to `tool_running` /
|
|
13
|
+
* `thinking` the moment the FIRST approval resolves even though a
|
|
14
|
+
* second is still waiting (session.ts resolves each canUseTool with
|
|
15
|
+
* `setStatus(approved ? "tool_running" : "thinking")`), and a strict
|
|
16
|
+
* gate would hide that second approval bar forever — deadlocking the
|
|
17
|
+
* turn from the user's point of view.
|
|
18
|
+
*
|
|
19
|
+
* 2. Turn-bounded backward scan. Pending approvals live in the CURRENT
|
|
20
|
+
* turn — every earlier turn's tool calls were finalized (executing /
|
|
21
|
+
* cancelled) before the next user message could be accepted. So scan
|
|
22
|
+
* backward from the tail and stop at the first `user` message. Within
|
|
23
|
+
* that window the OLDEST pending match wins, matching the previous
|
|
24
|
+
* forward-scan semantics (the daemon serializes approvals oldest
|
|
25
|
+
* first).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type { SessionMessage, SessionStatus } from "@codeoid/protocol";
|
|
29
|
+
|
|
30
|
+
const APPROVAL_POSSIBLE: ReadonlySet<SessionStatus> = new Set<SessionStatus>([
|
|
31
|
+
"waiting_approval",
|
|
32
|
+
"thinking",
|
|
33
|
+
"tool_running",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Find the oldest tool_call in `waiting_confirmation` within the current
|
|
38
|
+
* turn, or null. `status` is the session's current status (undefined when
|
|
39
|
+
* the session record is missing).
|
|
40
|
+
*/
|
|
41
|
+
export function findPendingApproval(
|
|
42
|
+
messages: readonly SessionMessage[],
|
|
43
|
+
status: SessionStatus | undefined,
|
|
44
|
+
): SessionMessage | null {
|
|
45
|
+
if (!status || !APPROVAL_POSSIBLE.has(status)) return null;
|
|
46
|
+
let oldest: SessionMessage | null = null;
|
|
47
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
48
|
+
const m = messages[i];
|
|
49
|
+
if (!m) continue;
|
|
50
|
+
if (m.role === "user") break; // turn boundary — nothing pends earlier
|
|
51
|
+
if (
|
|
52
|
+
m.role === "tool_call" &&
|
|
53
|
+
m.tool &&
|
|
54
|
+
m.tool.state.phase === "waiting_confirmation"
|
|
55
|
+
) {
|
|
56
|
+
oldest = m; // keep walking — an earlier pending in this turn wins
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return oldest;
|
|
60
|
+
}
|