@vezham/tamizhi-client 0.0.0-stage → 26.3.1
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/LICENSE +173 -0
- package/README.md +106 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/lib/client.d.ts +51 -0
- package/dist/lib/client.d.ts.map +1 -0
- package/dist/lib/client.js +185 -0
- package/dist/lib/companion-connect.d.ts +9 -0
- package/dist/lib/companion-connect.d.ts.map +1 -0
- package/dist/lib/companion-connect.js +109 -0
- package/dist/lib/companion-transport.d.ts +10 -0
- package/dist/lib/companion-transport.d.ts.map +1 -0
- package/dist/lib/companion-transport.js +36 -0
- package/dist/lib/companion.d.ts +3 -0
- package/dist/lib/companion.d.ts.map +1 -0
- package/dist/lib/companion.js +13 -0
- package/dist/lib/connection.d.ts +17 -0
- package/dist/lib/connection.d.ts.map +1 -0
- package/dist/lib/connection.js +125 -0
- package/dist/lib/stream.d.ts +3 -0
- package/dist/lib/stream.d.ts.map +1 -0
- package/dist/lib/stream.js +43 -0
- package/package.json +34 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
|
|
3
|
+
Business Source License 1.1
|
|
4
|
+
|
|
5
|
+
License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved.
|
|
6
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
|
|
12
|
+
Licensor: Vezham Technologies Private Limited
|
|
13
|
+
|
|
14
|
+
Licensed Work: The source code and packages in this repository (“the Licensed Work”).
|
|
15
|
+
The Licensed Work is (c) 2025–present Vezham Technologies Private Limited
|
|
16
|
+
|
|
17
|
+
Release Tag: The applicable release tag or commit identifier (for example, “v1.0.0”)
|
|
18
|
+
published by the Licensor for this version of the Licensed Work.
|
|
19
|
+
Users must cross-check the tag or commit identifier to verify
|
|
20
|
+
the release version and applicable Change Date.
|
|
21
|
+
|
|
22
|
+
Additional Use Grant: 1. You may make non-production use of the Licensed Work.
|
|
23
|
+
|
|
24
|
+
2. You may make production use of the Licensed Work, provided
|
|
25
|
+
your use does not include offering the Licensed Work, or any
|
|
26
|
+
derivative thereof, to third parties on a hosted, embedded,
|
|
27
|
+
or managed basis in order to compete with the Licensor’s
|
|
28
|
+
commercial offerings based on the Licensed Work, or offerings
|
|
29
|
+
that provide substantially similar functionality.
|
|
30
|
+
|
|
31
|
+
For purposes of this license:
|
|
32
|
+
|
|
33
|
+
A “competitive offering” is a Product that is offered to third
|
|
34
|
+
parties on a paid basis, including through paid support,
|
|
35
|
+
consulting, managed service arrangements, or similar commercial
|
|
36
|
+
relationships, that significantly overlaps with the capabilities
|
|
37
|
+
or functionality of the Licensor’s commercial offerings based on
|
|
38
|
+
the Licensed Work.
|
|
39
|
+
|
|
40
|
+
If your Product is not a competitive offering when you first
|
|
41
|
+
make it generally available, it will not become a competitive
|
|
42
|
+
offering solely because the Licensor releases a new version of the
|
|
43
|
+
Licensed Work with additional features or capabilities. Products
|
|
44
|
+
that are not provided on a paid basis are not competitive offerings.
|
|
45
|
+
|
|
46
|
+
“Product” means any software or service that is (a) offered to end
|
|
47
|
+
users for deployment in their own environments (self-managed), or
|
|
48
|
+
(b) offered as a hosted, cloud, or managed service by you or on
|
|
49
|
+
your behalf.
|
|
50
|
+
|
|
51
|
+
“Embedded” means including the source code or executable code of
|
|
52
|
+
the Licensed Work in a competitive offering. “Embedded” also means
|
|
53
|
+
packaging or distributing the competitive offering in a way that
|
|
54
|
+
requires the Licensed Work to be accessed, invoked, or downloaded
|
|
55
|
+
for the competitive offering to function.
|
|
56
|
+
|
|
57
|
+
Hosting or using the Licensed Work internally within your own
|
|
58
|
+
organization, including your affiliates under common control,
|
|
59
|
+
is not considered a competitive offering.
|
|
60
|
+
|
|
61
|
+
3. Educational institutions and legally recognised non-profit
|
|
62
|
+
organizations may use the Licensed Work in a production
|
|
63
|
+
environment without restriction, provided such use is not
|
|
64
|
+
for commercial profit.
|
|
65
|
+
|
|
66
|
+
For binding interpretive guidance on using Vezham products under
|
|
67
|
+
the Business Source License, please visit our licensing FAQ:
|
|
68
|
+
https://trust.vezham.com/legal/license-faq
|
|
69
|
+
|
|
70
|
+
Change Date: Four (4) years from the date the Licensed Work is first publicly
|
|
71
|
+
released under the specified Release Tag
|
|
72
|
+
|
|
73
|
+
Change License: GNU Affero General Public License v3.0 (AGPL-3.0-or-later)
|
|
74
|
+
The full text of the Change License is available at:
|
|
75
|
+
https://www.gnu.org/licenses/agpl-3.0.html
|
|
76
|
+
|
|
77
|
+
For information about commercial licensing or alternative arrangements,
|
|
78
|
+
please contact the Licensor at: https://vezham.com
|
|
79
|
+
or email: license@vezham.com
|
|
80
|
+
|
|
81
|
+
For formal legal notices or IP-related correspondence,
|
|
82
|
+
please contact: licensor@vezham.com
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
Notice
|
|
87
|
+
|
|
88
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
89
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
90
|
+
under an Open Source License, as stated in this License.
|
|
91
|
+
|
|
92
|
+
For more information on the use of the Business Source License generally,
|
|
93
|
+
please visit the Adopting and Developing Business Source License FAQ at:
|
|
94
|
+
https://mariadb.com/bsl-faq-adopting
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
Business Source License 1.1
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
Terms
|
|
103
|
+
|
|
104
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
105
|
+
works, redistribute, and make non-production use of the Licensed Work.
|
|
106
|
+
The Licensor may make an Additional Use Grant, above, permitting limited
|
|
107
|
+
production use.
|
|
108
|
+
|
|
109
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
110
|
+
available distribution of a specific version of the Licensed Work under this
|
|
111
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
112
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
113
|
+
above terminate.
|
|
114
|
+
|
|
115
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
116
|
+
currently in effect as described in this License, you must purchase a
|
|
117
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
118
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
119
|
+
|
|
120
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
121
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
122
|
+
separately for each version of the Licensed Work, and the Change Date may
|
|
123
|
+
vary for each version of the Licensed Work released by the Licensor.
|
|
124
|
+
|
|
125
|
+
You must conspicuously display this License on each original or modified copy
|
|
126
|
+
of the Licensed Work. If you receive the Licensed Work in original or modified
|
|
127
|
+
form from a third party, the terms and conditions set forth in this License
|
|
128
|
+
apply to your use of that work.
|
|
129
|
+
|
|
130
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
131
|
+
terminate your rights under this License for the current and all other versions
|
|
132
|
+
of the Licensed Work.
|
|
133
|
+
|
|
134
|
+
This License does not grant you any right in any trademark or logo of the
|
|
135
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
136
|
+
the Licensor as expressly required by this License).
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
Disclaimer
|
|
141
|
+
|
|
142
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
143
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
144
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
145
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
146
|
+
TITLE.
|
|
147
|
+
|
|
148
|
+
-----------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
MariaDB hereby grants you permission to use this License’s text to license your
|
|
151
|
+
works, and to refer to it using the trademark "Business Source License", as long
|
|
152
|
+
as you comply with the Covenants of Licensor below.
|
|
153
|
+
|
|
154
|
+
Covenants of Licensor
|
|
155
|
+
|
|
156
|
+
In consideration of the right to use this License’s text and the "Business
|
|
157
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
158
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
159
|
+
|
|
160
|
+
1. To specify as the Change License the GPL Version 3.0 or any later version,
|
|
161
|
+
or a license that is compatible with GPL Version 3.0 or a later version,
|
|
162
|
+
where "compatible" means that software provided under the Change License can
|
|
163
|
+
be included in a program with software provided under GPL Version 3.0 or a
|
|
164
|
+
later version. Licensor may specify additional Change Licenses without
|
|
165
|
+
limitation.
|
|
166
|
+
|
|
167
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
168
|
+
impose any additional restriction on the right granted in this License, as
|
|
169
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
170
|
+
|
|
171
|
+
3. To specify a Change Date.
|
|
172
|
+
|
|
173
|
+
4. Not to modify this License in any other way.
|
package/README.md
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tamizhi client and local companion
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The standalone and embedded chat share this client. It connects to the Go
|
|
4
|
+
companion over loopback HTTP/SSE and exposes `createTamizhiConnection` for
|
|
5
|
+
TanStack AI. The normal Tamizhi UI uses `runTamizhiChat`, which lets TanStack
|
|
6
|
+
assemble live messages while preserving the existing durable history, profiles,
|
|
7
|
+
approvals, tools and sources. Go owns the only agent loop.
|
|
8
|
+
|
|
9
|
+
## Start locally
|
|
10
|
+
|
|
11
|
+
In the Go backend checkout:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm exec nx run tamizhi-api:build
|
|
15
|
+
./dist/tamizhi serve
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Open **Connect companion** beside the composer in standalone or embed and click
|
|
19
|
+
**Connect**. The browser supplies its current origin. For an unknown origin,
|
|
20
|
+
compare the verification code and origin shown in the UI with the CLI prompt,
|
|
21
|
+
then enter `y` to approve and remember it, or `n`/Enter to reject. No terminal
|
|
22
|
+
input means rejection. `--origin` is optional preapproval, not a required UI flag.
|
|
23
|
+
|
|
24
|
+
Approved origins are saved in owner-only `~/.vezham/tamizhi/allowed-origins.json`
|
|
25
|
+
(or the selected `--data-dir`). Connections expire after five minutes; closing
|
|
26
|
+
or cancelling the modal cancels its request. Reloading the UI or restarting the
|
|
27
|
+
CLI requires clicking **Connect** again, but remembered origins do not prompt.
|
|
28
|
+
The verification code is for comparison, not something to copy into an input.
|
|
29
|
+
|
|
30
|
+
The default endpoint is `http://127.0.0.1:7331/api/tamizhi`. Hosted apps require
|
|
31
|
+
HTTPS and may need browser local-network permission; localhost connects directly.
|
|
32
|
+
HTTP `*.vezham.local` apps use the shared Vite development relay. After pairing,
|
|
33
|
+
**AI settings** beside the connection control configures BYOK providers. Leo is the default built-in tool-workflow assistant and requires no model-provider
|
|
34
|
+
key. Optional BYOK models provide broader reasoning and language generation. No extension or hosted
|
|
35
|
+
Tamizhi service is required for local research.
|
|
36
|
+
|
|
37
|
+
CLI clients use the same runtime:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
./dist/tamizhi status
|
|
41
|
+
./dist/tamizhi chat --permission safe 'What is the latest React version?'
|
|
42
|
+
./dist/tamizhi chat --permission safe --json 'What is the latest React version?'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The default permission mode is `ask`; terminal approvals read `y`/`N` from stdin.
|
|
46
|
+
`--json` emits structured stream events, and human progress/prompts go to stderr.
|
|
47
|
+
Ctrl+C stops the active request and attempts to cancel its backend run. `serve`,
|
|
48
|
+
`pair`, `status` and `chat` accept `--data-dir` and `--listen` as applicable; see
|
|
49
|
+
`./dist/tamizhi <command> --help`. State defaults to `~/.vezham/tamizhi/`:
|
|
50
|
+
SQLite conversations, an encryption key, and an owner-only CLI token. No Neon
|
|
51
|
+
setup is needed for local mode. `tamizhi api --config path` retains configured
|
|
52
|
+
hosted API/OIDC/Neon operation when wanted.
|
|
53
|
+
|
|
54
|
+
## Connection boundaries
|
|
55
|
+
|
|
56
|
+
- The service binds only to loopback. Only the bounded connection handshake is
|
|
57
|
+
exposed to an unknown origin; all normal API requests require a trusted origin
|
|
58
|
+
and authentication. Approval is saved before granting access.
|
|
59
|
+
- Connection capabilities are one-use, origin-bound and expire after five minutes.
|
|
60
|
+
UI sessions expire
|
|
61
|
+
after eight hours, on explicit disconnect, or when the companion restarts.
|
|
62
|
+
- UI bearer credentials stay in memory; the endpoint preference alone uses
|
|
63
|
+
localStorage. A reload requires re-pairing. CLI secrets never enter UI code.
|
|
64
|
+
- The browser grants local-network access; CORS and that permission are separate.
|
|
65
|
+
- If the companion is stopped or pairing expires, restart/re-pair through the
|
|
66
|
+
visible connection control. The local demo remains available when disconnected.
|
|
67
|
+
- The public web connector reads server-rendered HTML/plain text, with SSRF,
|
|
68
|
+
byte/source/operation/time limits. It does not use your browser login or render
|
|
69
|
+
JavaScript. CAPTCHA, blocked sites and search-provider errors return failures.
|
|
70
|
+
|
|
71
|
+
## TanStack integration
|
|
72
|
+
|
|
73
|
+
`createTamizhiConnection(client, options)` returns a TanStack
|
|
74
|
+
`ConnectConnectionAdapter`, usable as the `connection` for `useChat` or a headless
|
|
75
|
+
`ChatClient`. It maps Go start/text/tool/result/finish/error events into AG-UI.
|
|
76
|
+
Tamizhi-specific progress, approval and source events remain available through
|
|
77
|
+
`onEvent`. Pairing/authentication and provider secrets belong to the companion;
|
|
78
|
+
this adapter does not implement TanStack's separate provider-key forwarding flow.
|
|
79
|
+
|
|
80
|
+
The existing low-level `createTamizhiClient` still owns authenticated requests,
|
|
81
|
+
strict event validation, sequence deduplication and reconnect/replay. Aborting a
|
|
82
|
+
TanStack request aborts its transport. The shared UI and CLI additionally cancel
|
|
83
|
+
the active Go run explicitly. Both standalone and embed retain HeroUI components.
|
|
84
|
+
|
|
85
|
+
## Verify
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
pnpm exec nx run-many -t test,typecheck,lint -p tamizhi-client,tamizhi-contracts,tamizhi-ui
|
|
89
|
+
TAMIZHI_BACKEND_DIR=/path/to/vx-be-kit-lab pnpm exec nx run tamizhi-client:companion-test
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The end-to-end fixture uses a real compiled CLI, pairing, Go HTTP/SSE, TanStack
|
|
93
|
+
and Chromium with isolated temporary state. Inference and retrieved sites are
|
|
94
|
+
controlled fixtures. It checks streamed answer/source delivery, CLI chat, and
|
|
95
|
+
connection replay rejection, terminal approve/reject, and saved-origin reconnect
|
|
96
|
+
after a real CLI restart. Live search/provider quality requires separate BYOK
|
|
97
|
+
verification; these tests consume no real provider quota.
|
|
98
|
+
|
|
99
|
+
HTTP `*.vezham.local` development apps can use the shared Vite app preset's
|
|
100
|
+
local relay with the default companion URL. Restart the dev server once after
|
|
101
|
+
this preset update. The relay preserves the app's origin for CLI approval and
|
|
102
|
+
authenticated requests. The relay is development-only;
|
|
103
|
+
production and preview still require HTTPS or localhost for direct connections.
|
|
104
|
+
|
|
105
|
+
Leo is advertised by the normal model catalog as `leo/default`. The same TanStack
|
|
106
|
+
adapter streams its responses and tool events. It uses Go workflow planners and
|
|
107
|
+
tool results, with no separate frontend mock or second execution loop.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAE5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAE/B,cAAc,2BAA2B,CAAA;AAEzC,cAAc,yBAAyB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { readEventStream } from "./lib/stream.js";
|
|
2
|
+
import { createTamizhiClient } from "./lib/client.js";
|
|
3
|
+
import { createTamizhiConnection, runTamizhiChat } from "./lib/connection.js";
|
|
4
|
+
import { DEFAULT_COMPANION_URL, companionURL } from "./lib/companion.js";
|
|
5
|
+
import { companionTransport } from "./lib/companion-transport.js";
|
|
6
|
+
import { connectCompanion } from "./lib/companion-connect.js";
|
|
7
|
+
export { DEFAULT_COMPANION_URL, companionTransport, companionURL, connectCompanion, createTamizhiClient, createTamizhiConnection, readEventStream, runTamizhiChat };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ApprovalDecision, ModelInfo, RunRequest, StreamEvent, TamizhiAPI } from '@vezham/tamizhi-contracts';
|
|
2
|
+
export interface TamizhiClient {
|
|
3
|
+
stream: (request: RunRequest, signal?: AbortSignal) => AsyncIterable<StreamEvent>;
|
|
4
|
+
conversation: (id: string, signal?: AbortSignal) => Promise<import('@vezham/tamizhi-contracts').StoredConversationData>;
|
|
5
|
+
models: (signal?: AbortSignal) => Promise<ModelInfo[]>;
|
|
6
|
+
subscribeProviderChanges?: (listener: () => void) => () => void;
|
|
7
|
+
api?: TamizhiAPIClient;
|
|
8
|
+
approve: (decision: ApprovalDecision, signal?: AbortSignal) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export interface TamizhiAPIClient {
|
|
11
|
+
authorizeProvider: (id: string, projectId: string, returnTo: string) => Promise<TamizhiAPI.ProviderAuthorization>;
|
|
12
|
+
providers: (signal?: AbortSignal) => Promise<TamizhiAPI.ProviderSettings[]>;
|
|
13
|
+
connectProvider: (id: string, apiKey: string, signal?: AbortSignal) => Promise<TamizhiAPI.ProviderSettings>;
|
|
14
|
+
setProviderSearch: (id: string, enabled: boolean) => Promise<TamizhiAPI.ProviderSettings>;
|
|
15
|
+
removeProvider: (id: string) => Promise<void>;
|
|
16
|
+
capabilities: (signal?: AbortSignal) => Promise<TamizhiAPI.Capabilities>;
|
|
17
|
+
session: (signal?: AbortSignal) => Promise<TamizhiAPI.SessionInfo>;
|
|
18
|
+
authSession: (signal?: AbortSignal) => Promise<TamizhiAPI.AuthSession>;
|
|
19
|
+
getLoginURL: (returnTo?: string, connection?: string) => string;
|
|
20
|
+
loginURL: string;
|
|
21
|
+
logout: () => Promise<void>;
|
|
22
|
+
conversations: (query?: string, cursor?: string, signal?: AbortSignal) => Promise<TamizhiAPI.ConversationPage>;
|
|
23
|
+
rename: (id: string, title: string) => Promise<TamizhiAPI.Conversation>;
|
|
24
|
+
deleteConversation: (id: string) => Promise<void>;
|
|
25
|
+
feedback: (id: string, message: string, rating: string) => Promise<TamizhiAPI.Conversation>;
|
|
26
|
+
tasks: (id: string, message: string, tasks: TamizhiAPI.Task[]) => Promise<TamizhiAPI.Conversation>;
|
|
27
|
+
branch: (id: string, messageId: string) => Promise<TamizhiAPI.Conversation>;
|
|
28
|
+
checkpoints: (id: string) => Promise<TamizhiAPI.Checkpoint[]>;
|
|
29
|
+
checkpoint: (id: string, label: string) => Promise<TamizhiAPI.Checkpoint>;
|
|
30
|
+
restore: (id: string, checkpoint: string) => Promise<TamizhiAPI.Conversation>;
|
|
31
|
+
compact: (id: string) => Promise<TamizhiAPI.Conversation>;
|
|
32
|
+
upload: (file: File, signal?: AbortSignal) => Promise<TamizhiAPI.Attachment>;
|
|
33
|
+
transcribe: (file: File, signal?: AbortSignal) => Promise<TamizhiAPI.Transcript>;
|
|
34
|
+
speech: (text: string, signal?: AbortSignal) => Promise<Blob>;
|
|
35
|
+
memory: (query?: string) => Promise<TamizhiAPI.Memory[]>;
|
|
36
|
+
remember: (text: string, tags?: string[]) => Promise<TamizhiAPI.Memory>;
|
|
37
|
+
forget: (id: string) => Promise<void>;
|
|
38
|
+
plugins: () => Promise<TamizhiAPI.PluginInfo[]>;
|
|
39
|
+
setPlugin: (name: string, enabled: boolean) => Promise<TamizhiAPI.PluginInfo[]>;
|
|
40
|
+
run: (id: string, signal?: AbortSignal) => Promise<TamizhiAPI.RunInfo>;
|
|
41
|
+
cancel: (id: string) => Promise<void>;
|
|
42
|
+
replay: (id: string, after?: number, signal?: AbortSignal) => AsyncIterable<StreamEvent>;
|
|
43
|
+
}
|
|
44
|
+
export interface ClientOptions {
|
|
45
|
+
baseURL: string;
|
|
46
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
47
|
+
credentials?: RequestCredentials;
|
|
48
|
+
fetch?: typeof globalThis.fetch;
|
|
49
|
+
}
|
|
50
|
+
export declare const createTamizhiClient: (options: ClientOptions) => TamizhiClient;
|
|
51
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { readEventStream } from "./stream.js";
|
|
2
|
+
import { approvalDecisionSchema, modelInfoSchema, runRequestSchema, storedConversationSchema } from "@vezham/tamizhi-contracts";
|
|
3
|
+
//#region src/lib/client.ts
|
|
4
|
+
var createTamizhiClient = (options) => {
|
|
5
|
+
const providerListeners = /* @__PURE__ */ new Set();
|
|
6
|
+
const changed = () => {
|
|
7
|
+
for (const listener of providerListeners) listener();
|
|
8
|
+
};
|
|
9
|
+
const base = options.baseURL.replace(/\/$/, "");
|
|
10
|
+
const fetchRequest = async (path, init) => {
|
|
11
|
+
const headers = new Headers(typeof options.headers === "function" ? await options.headers() : options.headers);
|
|
12
|
+
headers.set("Accept", "application/json");
|
|
13
|
+
if (typeof init.body === "string") headers.set("Content-Type", "application/json");
|
|
14
|
+
if (path === "/chat" || path.includes("/events")) headers.set("Accept", "text/event-stream");
|
|
15
|
+
const response = await (options.fetch ?? globalThis.fetch)(`${base}${path}`, {
|
|
16
|
+
...init,
|
|
17
|
+
headers,
|
|
18
|
+
credentials: options.credentials ?? "same-origin"
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const details = await response.json().catch(() => null);
|
|
22
|
+
const message = details && typeof details === "object" && "message" in details && typeof details.message === "string" ? details.message.slice(0, 500) : "";
|
|
23
|
+
throw new Error(`Tamizhi request failed (${response.status})${message ? `: ${message}` : ""}`);
|
|
24
|
+
}
|
|
25
|
+
return response;
|
|
26
|
+
};
|
|
27
|
+
const json = async (path, init = {}) => (await fetchRequest(path, init)).json();
|
|
28
|
+
const body = (value, method = "POST") => ({
|
|
29
|
+
method,
|
|
30
|
+
body: JSON.stringify(value)
|
|
31
|
+
});
|
|
32
|
+
const segment = encodeURIComponent;
|
|
33
|
+
const sessionPath = (id) => `/conversations/${segment(id)}`;
|
|
34
|
+
const upload = async (path, file, signal) => {
|
|
35
|
+
const data = new FormData();
|
|
36
|
+
data.set("file", file);
|
|
37
|
+
return json(path, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
body: data,
|
|
40
|
+
signal
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const replay = async function* (id, after = 0, signal) {
|
|
44
|
+
const response = await fetchRequest(`/runs/${segment(id)}/events?after=${after}`, { signal });
|
|
45
|
+
if (!response.body) throw new Error("Expected a Tamizhi event stream");
|
|
46
|
+
yield* readEventStream(response.body, signal);
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
subscribeProviderChanges: (listener) => {
|
|
50
|
+
providerListeners.add(listener);
|
|
51
|
+
return () => {
|
|
52
|
+
providerListeners.delete(listener);
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
api: {
|
|
56
|
+
authorizeProvider: (id, projectId, returnTo) => json(`/providers/${segment(id)}/oauth`, body({
|
|
57
|
+
projectId,
|
|
58
|
+
returnTo
|
|
59
|
+
})),
|
|
60
|
+
providers: (signal) => json("/providers", { signal }),
|
|
61
|
+
connectProvider: async (id, apiKey, signal) => {
|
|
62
|
+
const result = await json(`/providers/${segment(id)}`, {
|
|
63
|
+
...body({ apiKey }, "PUT"),
|
|
64
|
+
signal
|
|
65
|
+
});
|
|
66
|
+
changed();
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
setProviderSearch: async (id, enabled) => {
|
|
70
|
+
const result = await json(`/providers/${segment(id)}/search`, body({ enabled }, "PUT"));
|
|
71
|
+
changed();
|
|
72
|
+
return result;
|
|
73
|
+
},
|
|
74
|
+
removeProvider: async (id) => {
|
|
75
|
+
await fetchRequest(`/providers/${segment(id)}`, { method: "DELETE" });
|
|
76
|
+
changed();
|
|
77
|
+
},
|
|
78
|
+
capabilities: (signal) => json("/capabilities", { signal }),
|
|
79
|
+
session: (signal) => json("/session", { signal }),
|
|
80
|
+
authSession: (signal) => json("/auth/session", { signal }),
|
|
81
|
+
getLoginURL: (returnTo, connection) => {
|
|
82
|
+
const query = new URLSearchParams();
|
|
83
|
+
if (returnTo) query.set("returnTo", returnTo);
|
|
84
|
+
if (connection) query.set("connection", connection);
|
|
85
|
+
return `${base}/auth/login${query.size ? `?${query}` : ""}`;
|
|
86
|
+
},
|
|
87
|
+
loginURL: `${base}/auth/login`,
|
|
88
|
+
logout: async () => {
|
|
89
|
+
await fetchRequest("/auth/logout", { method: "POST" });
|
|
90
|
+
},
|
|
91
|
+
conversations: (query = "", cursor = "", signal) => json(`/conversations?q=${segment(query)}&cursor=${segment(cursor)}`, { signal }),
|
|
92
|
+
rename: (id, title) => json(sessionPath(id), body({ title }, "PATCH")),
|
|
93
|
+
deleteConversation: async (id) => {
|
|
94
|
+
await fetchRequest(sessionPath(id), { method: "DELETE" });
|
|
95
|
+
},
|
|
96
|
+
feedback: (id, message, rating) => json(`${sessionPath(id)}/messages/${segment(message)}/feedback`, body({ rating }, "PUT")),
|
|
97
|
+
tasks: (id, message, tasks) => json(`${sessionPath(id)}/messages/${segment(message)}/tasks`, body({ tasks }, "PUT")),
|
|
98
|
+
branch: (id, messageId) => json(`${sessionPath(id)}/branches`, body({ messageId })),
|
|
99
|
+
checkpoints: (id) => json(`${sessionPath(id)}/checkpoints`),
|
|
100
|
+
checkpoint: (id, label) => json(`${sessionPath(id)}/checkpoints`, body({ label })),
|
|
101
|
+
restore: (id, checkpoint) => json(`${sessionPath(id)}/checkpoints/${segment(checkpoint)}/restore`, { method: "POST" }),
|
|
102
|
+
compact: (id) => json(`${sessionPath(id)}/compact`, { method: "POST" }),
|
|
103
|
+
upload: (file, signal) => upload("/attachments", file, signal),
|
|
104
|
+
transcribe: (file, signal) => upload("/audio/transcriptions", file, signal),
|
|
105
|
+
speech: async (text, signal) => (await fetchRequest("/audio/speech", {
|
|
106
|
+
...body({ text }),
|
|
107
|
+
signal
|
|
108
|
+
})).blob(),
|
|
109
|
+
memory: (query = "") => json(`/memory?q=${segment(query)}`),
|
|
110
|
+
remember: (text, tags) => json("/memory", body({
|
|
111
|
+
text,
|
|
112
|
+
tags
|
|
113
|
+
})),
|
|
114
|
+
forget: async (id) => {
|
|
115
|
+
await fetchRequest(`/memory/${segment(id)}`, { method: "DELETE" });
|
|
116
|
+
},
|
|
117
|
+
plugins: () => json("/plugins"),
|
|
118
|
+
setPlugin: async (name, enabled) => {
|
|
119
|
+
const installed = await json(`/plugins/${segment(name)}`, body({ enabled }, "PATCH"));
|
|
120
|
+
changed();
|
|
121
|
+
return installed;
|
|
122
|
+
},
|
|
123
|
+
run: (id, signal) => json(`/runs/${segment(id)}`, { signal }),
|
|
124
|
+
cancel: async (id) => {
|
|
125
|
+
await fetchRequest(`/runs/${segment(id)}/cancel`, { method: "POST" });
|
|
126
|
+
},
|
|
127
|
+
replay
|
|
128
|
+
},
|
|
129
|
+
stream: async function* (request, signal) {
|
|
130
|
+
const input = runRequestSchema.parse(request);
|
|
131
|
+
const response = await fetchRequest("/chat", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: JSON.stringify(input),
|
|
134
|
+
signal
|
|
135
|
+
});
|
|
136
|
+
if (!response.headers.get("Content-Type")?.includes("text/event-stream") || !response.body) throw new Error("Expected a Tamizhi event stream");
|
|
137
|
+
let runId = response.headers.get("X-Tamizhi-Run-ID") ?? "";
|
|
138
|
+
let sequence = 0;
|
|
139
|
+
let current = response;
|
|
140
|
+
for (let attempt = 0;; attempt++) {
|
|
141
|
+
let terminal = false;
|
|
142
|
+
try {
|
|
143
|
+
if (!current.body) throw new Error("Expected a Tamizhi event stream");
|
|
144
|
+
for await (const event of readEventStream(current.body, signal)) {
|
|
145
|
+
runId = event.runId;
|
|
146
|
+
if (event.sequence !== void 0 && event.sequence <= sequence) continue;
|
|
147
|
+
sequence = event.sequence ?? sequence;
|
|
148
|
+
terminal ||= event.type === "finish" || event.type === "error";
|
|
149
|
+
yield event;
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (signal?.aborted || terminal || !runId || attempt >= 3) throw error;
|
|
154
|
+
await new Promise((resolve, reject) => {
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
reject(signal?.reason);
|
|
158
|
+
};
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
signal?.removeEventListener("abort", onAbort);
|
|
161
|
+
resolve();
|
|
162
|
+
}, Math.min(500 * 2 ** attempt, 4e3));
|
|
163
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
});
|
|
165
|
+
current = await fetchRequest(`/runs/${segment(runId)}/events?after=${sequence}`, { signal });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
conversation: async (id, signal) => storedConversationSchema.parse(await (await fetchRequest(`/conversations/${encodeURIComponent(id)}`, { signal })).json()),
|
|
170
|
+
models: async (signal) => {
|
|
171
|
+
const data = await (await fetchRequest("/models", { signal })).json();
|
|
172
|
+
if (!Array.isArray(data)) throw new Error("Invalid model catalog");
|
|
173
|
+
return data.map((value) => modelInfoSchema.parse(value));
|
|
174
|
+
},
|
|
175
|
+
approve: async (decision, signal) => {
|
|
176
|
+
await fetchRequest("/approvals", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify(approvalDecisionSchema.parse(decision)),
|
|
179
|
+
signal
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
export { createTamizhiClient };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TamizhiAPI } from '@vezham/tamizhi-contracts';
|
|
2
|
+
export type CompanionConnectionPrompt = Pick<TamizhiAPI.CompanionConnection, 'code' | 'expiresAt'>;
|
|
3
|
+
export declare const connectCompanion: (options: {
|
|
4
|
+
baseURL: string;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
onPending?: (prompt: CompanionConnectionPrompt) => void;
|
|
8
|
+
}) => Promise<import("./client").TamizhiClient>;
|
|
9
|
+
//# sourceMappingURL=companion-connect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-connect.d.ts","sourceRoot":"","sources":["../../src/lib/companion-connect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAK3D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,UAAU,CAAC,mBAAmB,EAC9B,MAAM,GAAG,WAAW,CACrB,CAAA;AA4BD,eAAO,MAAM,gBAAgB,GAAU,SAAS;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,yBAAyB,KAAK,IAAI,CAAA;CACxD,8CA8FA,CAAA"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createTamizhiClient } from "./client.js";
|
|
2
|
+
import { companionURL } from "./companion.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/lib/companion-connect.ts
|
|
5
|
+
var connectionSchema = z.object({
|
|
6
|
+
requestId: z.string().min(32).max(128),
|
|
7
|
+
code: z.string().min(6).max(12),
|
|
8
|
+
expiresAt: z.string().datetime()
|
|
9
|
+
});
|
|
10
|
+
var resultSchema = z.object({
|
|
11
|
+
status: z.enum([
|
|
12
|
+
"pending",
|
|
13
|
+
"approved",
|
|
14
|
+
"rejected"
|
|
15
|
+
]),
|
|
16
|
+
session: z.object({
|
|
17
|
+
token: z.string().min(32).max(128),
|
|
18
|
+
expiresAt: z.string().datetime()
|
|
19
|
+
}).optional()
|
|
20
|
+
});
|
|
21
|
+
var waitForPoll = (signal) => new Promise((resolve, reject) => {
|
|
22
|
+
const abort = () => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
reject(signal.reason);
|
|
25
|
+
};
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
signal.removeEventListener("abort", abort);
|
|
28
|
+
resolve();
|
|
29
|
+
}, 750);
|
|
30
|
+
if (signal.aborted) abort();
|
|
31
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
32
|
+
});
|
|
33
|
+
var connectCompanion = async (options) => {
|
|
34
|
+
const base = companionURL(options.baseURL);
|
|
35
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
38
|
+
if (options.signal?.aborted) abort();
|
|
39
|
+
else options.signal?.addEventListener("abort", abort, { once: true });
|
|
40
|
+
const timeout = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Connection request expired. Click Connect to try again.")), 300 * 1e3);
|
|
41
|
+
let requestId;
|
|
42
|
+
let complete = false;
|
|
43
|
+
let notified = false;
|
|
44
|
+
const post = async (path, body) => {
|
|
45
|
+
controller.signal.throwIfAborted();
|
|
46
|
+
let response;
|
|
47
|
+
try {
|
|
48
|
+
response = await fetcher(base + "/auth/connect" + path, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
credentials: "omit",
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal: controller.signal
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
57
|
+
throw new Error("Cannot reach local Tamizhi. Start the companion and allow local network access in your browser.", { cause: error });
|
|
58
|
+
}
|
|
59
|
+
if (!response.ok) throw new Error(response.status === 409 ? "Another connection request is pending or the companion is busy. Try again shortly." : "Connection request expired or was refused by the companion. Click Connect to try again.");
|
|
60
|
+
return response.json();
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
const connection = connectionSchema.parse(await post("", {}));
|
|
64
|
+
requestId = connection.requestId;
|
|
65
|
+
if (Date.parse(connection.expiresAt) <= Date.now()) throw new Error("Connection request expired. Click Connect to try again.");
|
|
66
|
+
while (true) {
|
|
67
|
+
const result = resultSchema.parse(await post("/poll", { requestId }));
|
|
68
|
+
if (result.status === "rejected") {
|
|
69
|
+
complete = true;
|
|
70
|
+
throw new Error("Connection rejected in the Tamizhi CLI.");
|
|
71
|
+
}
|
|
72
|
+
if (result.status === "approved") {
|
|
73
|
+
if (!result.session) throw new Error("The companion returned an invalid session.");
|
|
74
|
+
complete = true;
|
|
75
|
+
return createTamizhiClient({
|
|
76
|
+
baseURL: base,
|
|
77
|
+
headers: { Authorization: "Bearer " + result.session.token },
|
|
78
|
+
credentials: "omit",
|
|
79
|
+
fetch: options.fetch
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!notified) {
|
|
83
|
+
notified = true;
|
|
84
|
+
options.onPending?.({
|
|
85
|
+
code: connection.code,
|
|
86
|
+
expiresAt: connection.expiresAt
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
await waitForPoll(controller.signal);
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
options.signal?.removeEventListener("abort", abort);
|
|
94
|
+
if (requestId && !complete) {
|
|
95
|
+
const cleanup = new AbortController();
|
|
96
|
+
const cleanupTimeout = setTimeout(() => cleanup.abort(), 3e3);
|
|
97
|
+
fetcher(base + "/auth/connect/cancel", {
|
|
98
|
+
method: "POST",
|
|
99
|
+
credentials: "omit",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ requestId }),
|
|
102
|
+
signal: cleanup.signal,
|
|
103
|
+
keepalive: true
|
|
104
|
+
}).catch(() => {}).finally(() => clearTimeout(cleanupTimeout));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { connectCompanion };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface Options {
|
|
2
|
+
baseURL: string;
|
|
3
|
+
origin: string;
|
|
4
|
+
isSecureContext: boolean;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare const companionTransport: (options: Options) => Promise<typeof globalThis.fetch>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=companion-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion-transport.d.ts","sourceRoot":"","sources":["../../src/lib/companion-transport.ts"],"names":[],"mappings":"AAIA,UAAU,OAAO;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,OAAO,CAAA;IACxB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC;AAED,eAAO,MAAM,kBAAkB,GAC7B,SAAS,OAAO,KACf,OAAO,CAAC,OAAO,UAAU,CAAC,KAAK,CAgEjC,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { companionURL } from "./companion.js";
|
|
2
|
+
//#region src/lib/companion-transport.ts
|
|
3
|
+
var INFO = "/__tamizhi/companion-info";
|
|
4
|
+
var PREFIX = "/__tamizhi/companion";
|
|
5
|
+
var companionTransport = async (options) => {
|
|
6
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
7
|
+
const base = companionURL(options.baseURL);
|
|
8
|
+
if (options.isSecureContext) return fetcher;
|
|
9
|
+
const app = new URL(options.origin);
|
|
10
|
+
const developmentHost = app.hostname.endsWith(".vezham.local") || [
|
|
11
|
+
"localhost",
|
|
12
|
+
"127.0.0.1",
|
|
13
|
+
"[::1]"
|
|
14
|
+
].includes(app.hostname);
|
|
15
|
+
if (app.protocol === "http:" && developmentHost && base === "http://127.0.0.1:7331/api/tamizhi") try {
|
|
16
|
+
const response = await fetcher(app.origin + INFO, {
|
|
17
|
+
credentials: "omit",
|
|
18
|
+
signal: options.signal,
|
|
19
|
+
headers: { Accept: "application/json" }
|
|
20
|
+
});
|
|
21
|
+
if (response.ok && response.headers.get("Content-Type")?.includes("application/json")) {
|
|
22
|
+
const info = await response.json();
|
|
23
|
+
if (info && typeof info === "object" && "protocol" in info && info.protocol === "tamizhi-dev-proxy/v1" && "path" in info && info.path === PREFIX && "target" in info && info.target === base) return (input, init) => {
|
|
24
|
+
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
25
|
+
if (url.href !== base && !url.href.startsWith(base + "/")) throw new Error("Development transport only supports its configured companion.");
|
|
26
|
+
const destination = app.origin + PREFIX + url.href.slice(base.length);
|
|
27
|
+
return fetcher(input instanceof Request ? new Request(destination, input) : destination, init);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (options.signal?.aborted) throw error;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("This HTTP origin cannot connect directly to the companion. For a .vezham.local Vite app, restart the dev server to enable its local relay. Otherwise use HTTPS or localhost.");
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { companionTransport };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"companion.d.ts","sourceRoot":"","sources":["../../src/lib/companion.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,sCAAsC,CAAA;AACxE,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,WAazC,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/companion.ts
|
|
2
|
+
var DEFAULT_COMPANION_URL = "http://127.0.0.1:7331/api/tamizhi";
|
|
3
|
+
var companionURL = (value) => {
|
|
4
|
+
const url = new URL(value);
|
|
5
|
+
if (url.protocol !== "http:" || ![
|
|
6
|
+
"127.0.0.1",
|
|
7
|
+
"localhost",
|
|
8
|
+
"[::1]"
|
|
9
|
+
].includes(url.hostname) || url.username || url.password || url.search || url.hash || url.pathname.replace(/\/$/, "") !== "/api/tamizhi") throw new Error("Use a loopback companion URL ending in /api/tamizhi.");
|
|
10
|
+
return url.href.replace(/\/$/, "");
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DEFAULT_COMPANION_URL, companionURL };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConnectConnectionAdapter } from '@tanstack/ai-client';
|
|
2
|
+
import type { RunRequest, StreamEvent } from '@vezham/tamizhi-contracts';
|
|
3
|
+
import type { TamizhiClient } from './client';
|
|
4
|
+
interface ConnectionOptions {
|
|
5
|
+
request?: RunRequest;
|
|
6
|
+
permission?: RunRequest['permission'];
|
|
7
|
+
model?: string;
|
|
8
|
+
fast?: boolean;
|
|
9
|
+
onEvent?: (event: StreamEvent) => void | Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export declare const createTamizhiConnection: (client: TamizhiClient, options?: ConnectionOptions) => ConnectConnectionAdapter;
|
|
12
|
+
export declare const runTamizhiChat: (client: TamizhiClient, request: RunRequest, callbacks: {
|
|
13
|
+
onText: (text: string) => void;
|
|
14
|
+
onEvent: (event: StreamEvent) => void | Promise<void>;
|
|
15
|
+
}, signal?: AbortSignal) => Promise<void>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/lib/connection.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAa,MAAM,qBAAqB,CAAA;AAG9E,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,UAAU,CAAA;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,CAAA;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvD;AAYD,eAAO,MAAM,uBAAuB,GAClC,QAAQ,aAAa,EACrB,UAAS,iBAAsB,KAC9B,wBAkFD,CAAA;AAGF,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,EACrB,SAAS,UAAU,EACnB,WAAW;IACT,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACtD,EACD,SAAS,WAAW,kBAyCrB,CAAA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { runRequestSchema } from "@vezham/tamizhi-contracts";
|
|
2
|
+
import { ChatClient } from "@tanstack/ai-client";
|
|
3
|
+
import { EventType } from "@tanstack/ai/client";
|
|
4
|
+
//#region src/lib/connection.ts
|
|
5
|
+
var inputText = (message) => {
|
|
6
|
+
if ("parts" in message) return message.parts.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.content : "").join("");
|
|
7
|
+
return typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
8
|
+
};
|
|
9
|
+
var createTamizhiConnection = (client, options = {}) => ({ async *connect(messages, _data, signal, context) {
|
|
10
|
+
const request = options.request ?? runRequestSchema.parse({
|
|
11
|
+
conversationId: context.threadId,
|
|
12
|
+
messages: messages.filter((message) => message.role === "user" || message.role === "assistant").map((message) => ({
|
|
13
|
+
id: message.id,
|
|
14
|
+
role: message.role,
|
|
15
|
+
content: inputText(message)
|
|
16
|
+
})),
|
|
17
|
+
permission: options.permission ?? "ask",
|
|
18
|
+
model: options.model,
|
|
19
|
+
fast: options.fast ?? false
|
|
20
|
+
});
|
|
21
|
+
let messageId = "";
|
|
22
|
+
let textStarted = false;
|
|
23
|
+
for await (const event of client.stream(request, signal)) {
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
await options.onEvent?.(event);
|
|
26
|
+
if (event.type === "start") yield {
|
|
27
|
+
type: EventType.RUN_STARTED,
|
|
28
|
+
threadId: event.conversationId,
|
|
29
|
+
runId: event.runId
|
|
30
|
+
};
|
|
31
|
+
else if (event.type === "text") {
|
|
32
|
+
if (!textStarted) {
|
|
33
|
+
messageId = event.runId + ":assistant";
|
|
34
|
+
textStarted = true;
|
|
35
|
+
yield {
|
|
36
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
37
|
+
messageId,
|
|
38
|
+
role: "assistant"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
yield {
|
|
42
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
43
|
+
messageId,
|
|
44
|
+
delta: event.delta
|
|
45
|
+
};
|
|
46
|
+
} else if (event.type === "tool") {
|
|
47
|
+
yield {
|
|
48
|
+
type: EventType.TOOL_CALL_START,
|
|
49
|
+
toolCallId: event.callId,
|
|
50
|
+
toolCallName: event.name
|
|
51
|
+
};
|
|
52
|
+
yield {
|
|
53
|
+
type: EventType.TOOL_CALL_ARGS,
|
|
54
|
+
toolCallId: event.callId,
|
|
55
|
+
delta: JSON.stringify(event.input)
|
|
56
|
+
};
|
|
57
|
+
yield {
|
|
58
|
+
type: EventType.TOOL_CALL_END,
|
|
59
|
+
toolCallId: event.callId
|
|
60
|
+
};
|
|
61
|
+
} else if (event.type === "tool-result") yield {
|
|
62
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
63
|
+
toolCallId: event.callId,
|
|
64
|
+
messageId: event.callId + ":result",
|
|
65
|
+
content: JSON.stringify(event.output),
|
|
66
|
+
role: "tool"
|
|
67
|
+
};
|
|
68
|
+
else if (event.type === "finish") {
|
|
69
|
+
if (textStarted) yield {
|
|
70
|
+
type: EventType.TEXT_MESSAGE_END,
|
|
71
|
+
messageId
|
|
72
|
+
};
|
|
73
|
+
yield {
|
|
74
|
+
type: EventType.RUN_FINISHED,
|
|
75
|
+
threadId: event.conversationId,
|
|
76
|
+
runId: event.runId
|
|
77
|
+
};
|
|
78
|
+
} else if (event.type === "error") yield {
|
|
79
|
+
type: EventType.RUN_ERROR,
|
|
80
|
+
message: event.message
|
|
81
|
+
};
|
|
82
|
+
else yield {
|
|
83
|
+
type: EventType.CUSTOM,
|
|
84
|
+
name: "tamizhi",
|
|
85
|
+
value: event
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} });
|
|
89
|
+
var runTamizhiChat = async (client, request, callbacks, signal) => {
|
|
90
|
+
let failure;
|
|
91
|
+
let previous = "";
|
|
92
|
+
const chat = new ChatClient({
|
|
93
|
+
threadId: request.conversationId,
|
|
94
|
+
connection: createTamizhiConnection(client, {
|
|
95
|
+
request,
|
|
96
|
+
onEvent: callbacks.onEvent
|
|
97
|
+
}),
|
|
98
|
+
onMessagesChange: (messages) => {
|
|
99
|
+
const text = [...messages].reverse().find((item) => item.role === "assistant")?.parts.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.content : "").join("");
|
|
100
|
+
if (text !== void 0 && text !== previous) {
|
|
101
|
+
previous = text;
|
|
102
|
+
callbacks.onText(text);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
onError: (error) => {
|
|
106
|
+
failure = error;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const abort = () => chat.stop();
|
|
110
|
+
if (signal?.aborted) {
|
|
111
|
+
chat.dispose();
|
|
112
|
+
signal.throwIfAborted();
|
|
113
|
+
}
|
|
114
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
115
|
+
try {
|
|
116
|
+
await chat.sendMessage(request.messages.at(-1)?.content ?? "");
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
if (failure) throw failure;
|
|
119
|
+
} finally {
|
|
120
|
+
signal?.removeEventListener("abort", abort);
|
|
121
|
+
chat.dispose();
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { createTamizhiConnection, runTamizhiChat };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/lib/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAE5D,eAAO,MAAM,eAAe,GAC1B,MAAM,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,WAAW,KACnB,cAAc,CAAC,WAAW,CA8C5B,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { streamEventSchema } from "@vezham/tamizhi-contracts";
|
|
2
|
+
//#region src/lib/stream.ts
|
|
3
|
+
var readEventStream = async function* (body, signal) {
|
|
4
|
+
const reader = body.getReader();
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
let buffer = "";
|
|
7
|
+
const abort = () => {
|
|
8
|
+
reader.cancel();
|
|
9
|
+
};
|
|
10
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
11
|
+
if (signal?.aborted) abort();
|
|
12
|
+
let finished = false;
|
|
13
|
+
try {
|
|
14
|
+
for (;;) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
const { value, done } = await reader.read();
|
|
17
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
18
|
+
if (buffer.length > 1e6) throw new Error("Stream event exceeds the maximum size");
|
|
19
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
20
|
+
let boundary = buffer.indexOf("\n\n");
|
|
21
|
+
while (boundary >= 0) {
|
|
22
|
+
const frame = buffer.slice(0, boundary);
|
|
23
|
+
buffer = buffer.slice(boundary + 2);
|
|
24
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
25
|
+
if (data) {
|
|
26
|
+
const event = streamEventSchema.parse(JSON.parse(data));
|
|
27
|
+
if (event.type === "finish" || event.type === "error") finished = true;
|
|
28
|
+
yield event;
|
|
29
|
+
}
|
|
30
|
+
boundary = buffer.indexOf("\n\n");
|
|
31
|
+
}
|
|
32
|
+
if (done) break;
|
|
33
|
+
}
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
if (!finished) throw new Error("The response stream ended before completion");
|
|
36
|
+
} finally {
|
|
37
|
+
signal?.removeEventListener("abort", abort);
|
|
38
|
+
await reader.cancel().catch(() => void 0);
|
|
39
|
+
reader.releaseLock();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { readEventStream };
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vezham/tamizhi-client",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "26.3.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/vezham/vx-fe-pkgs.git",
|
|
7
|
+
"directory": "packages/tamizhi/client"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"@vx/fe-kit": "./src/index.ts",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"!**/*.tsbuildinfo",
|
|
24
|
+
"dist",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@tanstack/ai": "1.0.0",
|
|
29
|
+
"@tanstack/ai-client": "1.0.0",
|
|
30
|
+
"@vezham/tamizhi-contracts": "26.3.1",
|
|
31
|
+
"zod": "4.6.7"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@playwright/test": "^1.64.1",
|
|
35
|
+
"vite": "8.1.6"
|
|
36
|
+
}
|
|
6
37
|
}
|