@simonfestl/husky-cli 0.8.2 → 0.9.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 +3 -116
- package/dist/commands/biz/customers.d.ts +8 -0
- package/dist/commands/biz/customers.js +181 -0
- package/dist/commands/biz/orders.d.ts +8 -0
- package/dist/commands/biz/orders.js +226 -0
- package/dist/commands/biz/products.d.ts +8 -0
- package/dist/commands/biz/products.js +255 -0
- package/dist/commands/biz/qdrant.d.ts +8 -0
- package/dist/commands/biz/qdrant.js +170 -0
- package/dist/commands/biz/seatable.d.ts +8 -0
- package/dist/commands/biz/seatable.js +449 -0
- package/dist/commands/biz/tickets.d.ts +8 -0
- package/dist/commands/biz/tickets.js +600 -0
- package/dist/commands/biz.d.ts +9 -0
- package/dist/commands/biz.js +22 -0
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +43 -16
- package/dist/commands/explain.js +12 -595
- package/dist/commands/idea.js +2 -50
- package/dist/commands/project.js +2 -47
- package/dist/commands/roadmap.js +0 -107
- package/dist/commands/task.js +11 -17
- package/dist/commands/vm.js +0 -225
- package/dist/commands/workflow.js +4 -60
- package/dist/index.js +5 -1
- package/dist/lib/biz/billbee-types.d.ts +259 -0
- package/dist/lib/biz/billbee-types.js +41 -0
- package/dist/lib/biz/billbee.d.ts +37 -0
- package/dist/lib/biz/billbee.js +165 -0
- package/dist/lib/biz/embeddings.d.ts +45 -0
- package/dist/lib/biz/embeddings.js +115 -0
- package/dist/lib/biz/index.d.ts +13 -0
- package/dist/lib/biz/index.js +11 -0
- package/dist/lib/biz/qdrant.d.ts +52 -0
- package/dist/lib/biz/qdrant.js +158 -0
- package/dist/lib/biz/seatable-types.d.ts +115 -0
- package/dist/lib/biz/seatable-types.js +27 -0
- package/dist/lib/biz/seatable.d.ts +49 -0
- package/dist/lib/biz/seatable.js +210 -0
- package/dist/lib/biz/zendesk-types.d.ts +136 -0
- package/dist/lib/biz/zendesk-types.js +28 -0
- package/dist/lib/biz/zendesk.d.ts +45 -0
- package/dist/lib/biz/zendesk.js +206 -0
- package/package.json +2 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Biz Library Exports
|
|
3
|
+
*/
|
|
4
|
+
export { BillbeeClient } from './billbee.js';
|
|
5
|
+
export { ZendeskClient } from './zendesk.js';
|
|
6
|
+
export { SeaTableClient } from './seatable.js';
|
|
7
|
+
export { QdrantClient } from './qdrant.js';
|
|
8
|
+
export { EmbeddingService, EMBEDDING_MODELS } from './embeddings.js';
|
|
9
|
+
export type { Point, SearchResult as QdrantSearchResult, SearchOptions, CollectionInfo, QdrantConfig } from './qdrant.js';
|
|
10
|
+
export type { EmbeddingConfig, EmbeddingResult } from './embeddings.js';
|
|
11
|
+
export * from './billbee-types.js';
|
|
12
|
+
export * from './zendesk-types.js';
|
|
13
|
+
export * from './seatable-types.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Biz Library Exports
|
|
3
|
+
*/
|
|
4
|
+
export { BillbeeClient } from './billbee.js';
|
|
5
|
+
export { ZendeskClient } from './zendesk.js';
|
|
6
|
+
export { SeaTableClient } from './seatable.js';
|
|
7
|
+
export { QdrantClient } from './qdrant.js';
|
|
8
|
+
export { EmbeddingService, EMBEDDING_MODELS } from './embeddings.js';
|
|
9
|
+
export * from './billbee-types.js';
|
|
10
|
+
export * from './zendesk-types.js';
|
|
11
|
+
export * from './seatable-types.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qdrant Vector Database Client
|
|
3
|
+
* Ported from TigerV0 with Husky Biz CLI integration
|
|
4
|
+
*/
|
|
5
|
+
export interface QdrantConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Point {
|
|
10
|
+
id: string | number;
|
|
11
|
+
vector: number[];
|
|
12
|
+
payload?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export interface SearchResult {
|
|
15
|
+
id: string | number;
|
|
16
|
+
score: number;
|
|
17
|
+
payload?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface SearchOptions {
|
|
20
|
+
limit?: number;
|
|
21
|
+
filter?: Record<string, unknown>;
|
|
22
|
+
scoreThreshold?: number;
|
|
23
|
+
offset?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface CollectionInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
vectorsCount: number;
|
|
28
|
+
pointsCount: number;
|
|
29
|
+
}
|
|
30
|
+
export declare class QdrantClient {
|
|
31
|
+
private url;
|
|
32
|
+
private apiKey?;
|
|
33
|
+
constructor(config: QdrantConfig);
|
|
34
|
+
/**
|
|
35
|
+
* Create client from Husky config
|
|
36
|
+
*/
|
|
37
|
+
static fromConfig(): QdrantClient;
|
|
38
|
+
private request;
|
|
39
|
+
listCollections(): Promise<string[]>;
|
|
40
|
+
getCollection(name: string): Promise<CollectionInfo>;
|
|
41
|
+
createCollection(name: string, vectorSize: number): Promise<void>;
|
|
42
|
+
deleteCollection(name: string): Promise<void>;
|
|
43
|
+
search(collectionName: string, vector: number[], limit?: number, options?: Omit<SearchOptions, 'limit'> & {
|
|
44
|
+
vectorName?: string;
|
|
45
|
+
}): Promise<SearchResult[]>;
|
|
46
|
+
upsert(collectionName: string, points: Point[]): Promise<void>;
|
|
47
|
+
upsertOne(collectionName: string, id: string | number, vector: number[], payload?: Record<string, unknown>): Promise<void>;
|
|
48
|
+
getPoint(collectionName: string, id: string | number): Promise<Point | null>;
|
|
49
|
+
deletePoints(collectionName: string, ids: (string | number)[]): Promise<void>;
|
|
50
|
+
count(collectionName: string): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
export default QdrantClient;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qdrant Vector Database Client
|
|
3
|
+
* Ported from TigerV0 with Husky Biz CLI integration
|
|
4
|
+
*/
|
|
5
|
+
import { getConfig } from '../../commands/config.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Qdrant Service (REST API based)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export class QdrantClient {
|
|
10
|
+
url;
|
|
11
|
+
apiKey;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.url = config.url.replace(/\/+$/, '');
|
|
14
|
+
this.apiKey = config.apiKey;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create client from Husky config
|
|
18
|
+
*/
|
|
19
|
+
static fromConfig() {
|
|
20
|
+
const config = getConfig();
|
|
21
|
+
const qdrantConfig = {
|
|
22
|
+
url: config.qdrantUrl || process.env.QDRANT_URL || 'http://localhost:6333',
|
|
23
|
+
apiKey: config.qdrantApiKey || process.env.QDRANT_API_KEY,
|
|
24
|
+
};
|
|
25
|
+
if (!qdrantConfig.url || qdrantConfig.url === 'http://localhost:6333') {
|
|
26
|
+
if (!process.env.QDRANT_URL) {
|
|
27
|
+
throw new Error('Missing Qdrant URL. Configure with:\n' +
|
|
28
|
+
' husky config set qdrant-url <url>\n' +
|
|
29
|
+
' husky config set qdrant-api-key <key>');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return new QdrantClient(qdrantConfig);
|
|
33
|
+
}
|
|
34
|
+
async request(path, options = {}) {
|
|
35
|
+
const url = `${this.url}${path}`;
|
|
36
|
+
const headers = {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
};
|
|
39
|
+
if (this.apiKey) {
|
|
40
|
+
headers['api-key'] = this.apiKey;
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
...options,
|
|
44
|
+
headers: {
|
|
45
|
+
...headers,
|
|
46
|
+
...options.headers,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const error = await response.text();
|
|
51
|
+
throw new Error(`Qdrant API Error ${response.status}: ${error}`);
|
|
52
|
+
}
|
|
53
|
+
return response.json();
|
|
54
|
+
}
|
|
55
|
+
// =========================================================================
|
|
56
|
+
// Collections
|
|
57
|
+
// =========================================================================
|
|
58
|
+
async listCollections() {
|
|
59
|
+
const response = await this.request('/collections');
|
|
60
|
+
return response.result.collections.map(c => c.name);
|
|
61
|
+
}
|
|
62
|
+
async getCollection(name) {
|
|
63
|
+
const response = await this.request(`/collections/${name}`);
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
vectorsCount: response.result.vectors_count,
|
|
67
|
+
pointsCount: response.result.points_count,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async createCollection(name, vectorSize) {
|
|
71
|
+
await this.request(`/collections/${name}`, {
|
|
72
|
+
method: 'PUT',
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
vectors: {
|
|
75
|
+
size: vectorSize,
|
|
76
|
+
distance: 'Cosine',
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async deleteCollection(name) {
|
|
82
|
+
await this.request(`/collections/${name}`, { method: 'DELETE' });
|
|
83
|
+
}
|
|
84
|
+
// =========================================================================
|
|
85
|
+
// Points
|
|
86
|
+
// =========================================================================
|
|
87
|
+
async search(collectionName, vector, limit = 10, options) {
|
|
88
|
+
// Build request body - support named vectors
|
|
89
|
+
const body = {
|
|
90
|
+
limit,
|
|
91
|
+
filter: options?.filter,
|
|
92
|
+
score_threshold: options?.scoreThreshold,
|
|
93
|
+
offset: options?.offset,
|
|
94
|
+
with_payload: true,
|
|
95
|
+
};
|
|
96
|
+
// If named vector, use object format
|
|
97
|
+
if (options?.vectorName) {
|
|
98
|
+
body.vector = { name: options.vectorName, vector };
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
body.vector = vector;
|
|
102
|
+
}
|
|
103
|
+
const response = await this.request(`/collections/${collectionName}/points/search`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
body: JSON.stringify(body),
|
|
106
|
+
});
|
|
107
|
+
return response.result.map(r => ({
|
|
108
|
+
id: r.id,
|
|
109
|
+
score: r.score,
|
|
110
|
+
payload: r.payload,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
async upsert(collectionName, points) {
|
|
114
|
+
await this.request(`/collections/${collectionName}/points?wait=true`, {
|
|
115
|
+
method: 'PUT',
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
points: points.map(p => ({
|
|
118
|
+
id: p.id,
|
|
119
|
+
vector: p.vector,
|
|
120
|
+
payload: p.payload || {},
|
|
121
|
+
})),
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async upsertOne(collectionName, id, vector, payload) {
|
|
126
|
+
await this.upsert(collectionName, [{ id, vector, payload }]);
|
|
127
|
+
}
|
|
128
|
+
async getPoint(collectionName, id) {
|
|
129
|
+
try {
|
|
130
|
+
const response = await this.request(`/collections/${collectionName}/points`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
ids: [id],
|
|
134
|
+
with_payload: true,
|
|
135
|
+
with_vector: true,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
if (response.result.length === 0)
|
|
139
|
+
return null;
|
|
140
|
+
const p = response.result[0];
|
|
141
|
+
return { id: p.id, vector: p.vector, payload: p.payload };
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async deletePoints(collectionName, ids) {
|
|
148
|
+
await this.request(`/collections/${collectionName}/points/delete?wait=true`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: JSON.stringify({ points: ids }),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async count(collectionName) {
|
|
154
|
+
const info = await this.getCollection(collectionName);
|
|
155
|
+
return info.pointsCount;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export default QdrantClient;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeaTable API Types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Client configuration
|
|
6
|
+
*/
|
|
7
|
+
export interface SeaTableConfig {
|
|
8
|
+
apiToken: string;
|
|
9
|
+
serverUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Column Types in SeaTable
|
|
13
|
+
*/
|
|
14
|
+
export declare enum ColumnType {
|
|
15
|
+
Text = "text",
|
|
16
|
+
Number = "number",
|
|
17
|
+
Checkbox = "checkbox",
|
|
18
|
+
Date = "date",
|
|
19
|
+
SingleSelect = "single-select",
|
|
20
|
+
MultipleSelect = "multiple-select",
|
|
21
|
+
Image = "image",
|
|
22
|
+
File = "file",
|
|
23
|
+
Email = "email",
|
|
24
|
+
URL = "url",
|
|
25
|
+
Duration = "duration",
|
|
26
|
+
Rating = "rating",
|
|
27
|
+
Formula = "formula",
|
|
28
|
+
Link = "link",
|
|
29
|
+
Creator = "creator",
|
|
30
|
+
CreatedTime = "ctime",
|
|
31
|
+
LastModifier = "last-modifier",
|
|
32
|
+
ModifiedTime = "mtime"
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* SeaTable Row
|
|
36
|
+
*/
|
|
37
|
+
export interface SeaTableRow {
|
|
38
|
+
_id: string;
|
|
39
|
+
_ctime?: string;
|
|
40
|
+
_mtime?: string;
|
|
41
|
+
_creator?: string;
|
|
42
|
+
_last_modifier?: string;
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* SeaTable Column
|
|
47
|
+
*/
|
|
48
|
+
export interface SeaTableColumn {
|
|
49
|
+
key: string;
|
|
50
|
+
name: string;
|
|
51
|
+
type: ColumnType;
|
|
52
|
+
width?: number;
|
|
53
|
+
editable?: boolean;
|
|
54
|
+
resizable?: boolean;
|
|
55
|
+
data?: {
|
|
56
|
+
link_id?: string;
|
|
57
|
+
other_table_id?: string;
|
|
58
|
+
options?: Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
color: string;
|
|
61
|
+
textColor?: string;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* SeaTable Table
|
|
67
|
+
*/
|
|
68
|
+
export interface SeaTableTable {
|
|
69
|
+
_id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
columns: SeaTableColumn[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* SeaTable Base Metadata
|
|
75
|
+
*/
|
|
76
|
+
export interface SeaTableMetadata {
|
|
77
|
+
tables: SeaTableTable[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Row Query Parameters
|
|
81
|
+
*/
|
|
82
|
+
export interface QueryRowsParams {
|
|
83
|
+
table_name: string;
|
|
84
|
+
view_name?: string;
|
|
85
|
+
order_by?: string;
|
|
86
|
+
direction?: 'asc' | 'desc';
|
|
87
|
+
start?: number;
|
|
88
|
+
limit?: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Filter Parameters
|
|
92
|
+
*/
|
|
93
|
+
export interface FilterParams {
|
|
94
|
+
filters?: Array<{
|
|
95
|
+
column_name: string;
|
|
96
|
+
filter_predicate: 'is' | 'is_not' | 'contains' | 'does_not_contain' | 'is_empty' | 'is_not_empty' | 'equal' | 'not_equal' | 'greater' | 'greater_or_equal' | 'less' | 'less_or_equal';
|
|
97
|
+
filter_term?: string | number | boolean;
|
|
98
|
+
}>;
|
|
99
|
+
filter_conjunction?: 'And' | 'Or';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Row Creation/Update Data
|
|
103
|
+
*/
|
|
104
|
+
export interface RowData {
|
|
105
|
+
[key: string]: string | number | boolean | string[] | null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Query Response
|
|
109
|
+
*/
|
|
110
|
+
export interface QueryResponse {
|
|
111
|
+
rows: SeaTableRow[];
|
|
112
|
+
metadata?: {
|
|
113
|
+
total_count?: number;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeaTable API Types
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Column Types in SeaTable
|
|
6
|
+
*/
|
|
7
|
+
export var ColumnType;
|
|
8
|
+
(function (ColumnType) {
|
|
9
|
+
ColumnType["Text"] = "text";
|
|
10
|
+
ColumnType["Number"] = "number";
|
|
11
|
+
ColumnType["Checkbox"] = "checkbox";
|
|
12
|
+
ColumnType["Date"] = "date";
|
|
13
|
+
ColumnType["SingleSelect"] = "single-select";
|
|
14
|
+
ColumnType["MultipleSelect"] = "multiple-select";
|
|
15
|
+
ColumnType["Image"] = "image";
|
|
16
|
+
ColumnType["File"] = "file";
|
|
17
|
+
ColumnType["Email"] = "email";
|
|
18
|
+
ColumnType["URL"] = "url";
|
|
19
|
+
ColumnType["Duration"] = "duration";
|
|
20
|
+
ColumnType["Rating"] = "rating";
|
|
21
|
+
ColumnType["Formula"] = "formula";
|
|
22
|
+
ColumnType["Link"] = "link";
|
|
23
|
+
ColumnType["Creator"] = "creator";
|
|
24
|
+
ColumnType["CreatedTime"] = "ctime";
|
|
25
|
+
ColumnType["LastModifier"] = "last-modifier";
|
|
26
|
+
ColumnType["ModifiedTime"] = "mtime";
|
|
27
|
+
})(ColumnType || (ColumnType = {}));
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeaTable API Client
|
|
3
|
+
* Ported from TigerV0 with Husky Biz CLI integration
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: SeaTable uses a 2-stage token system:
|
|
6
|
+
* 1. API Token (permanent) - generated in SeaTable UI
|
|
7
|
+
* 2. Base Token (3 days TTL) - generated from API Token automatically
|
|
8
|
+
*/
|
|
9
|
+
import type { SeaTableConfig, SeaTableRow, SeaTableMetadata, QueryRowsParams, FilterParams, RowData, QueryResponse } from './seatable-types.js';
|
|
10
|
+
export declare class SeaTableClient {
|
|
11
|
+
private serverUrl;
|
|
12
|
+
private apiToken;
|
|
13
|
+
private baseToken;
|
|
14
|
+
private baseTokenExpiry;
|
|
15
|
+
private dtableServer;
|
|
16
|
+
private dtableUuid;
|
|
17
|
+
constructor(config: SeaTableConfig);
|
|
18
|
+
/**
|
|
19
|
+
* Create client from Husky config
|
|
20
|
+
*/
|
|
21
|
+
static fromConfig(): SeaTableClient;
|
|
22
|
+
private isApiGateway;
|
|
23
|
+
private buildEndpoint;
|
|
24
|
+
/**
|
|
25
|
+
* Get a Base Token from the API Token
|
|
26
|
+
* Base Tokens are valid for 3 days
|
|
27
|
+
*/
|
|
28
|
+
private getBaseToken;
|
|
29
|
+
/**
|
|
30
|
+
* Make an authenticated API request
|
|
31
|
+
*/
|
|
32
|
+
private request;
|
|
33
|
+
getMetadata(): Promise<SeaTableMetadata>;
|
|
34
|
+
listRows(params: QueryRowsParams): Promise<SeaTableRow[]>;
|
|
35
|
+
getRow(tableName: string, rowId: string): Promise<SeaTableRow | null>;
|
|
36
|
+
queryRows(tableName: string, filters: FilterParams, params?: Partial<QueryRowsParams>): Promise<QueryResponse>;
|
|
37
|
+
searchRows(tableName: string, searchQuery: string): Promise<SeaTableRow[]>;
|
|
38
|
+
appendRow(tableName: string, row: RowData): Promise<SeaTableRow>;
|
|
39
|
+
updateRow(tableName: string, rowId: string, data: RowData): Promise<{
|
|
40
|
+
success: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
deleteRow(tableName: string, rowId: string): Promise<{
|
|
43
|
+
success: boolean;
|
|
44
|
+
}>;
|
|
45
|
+
deleteRows(tableName: string, rowIds: string[]): Promise<{
|
|
46
|
+
success: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
export default SeaTableClient;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeaTable API Client
|
|
3
|
+
* Ported from TigerV0 with Husky Biz CLI integration
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: SeaTable uses a 2-stage token system:
|
|
6
|
+
* 1. API Token (permanent) - generated in SeaTable UI
|
|
7
|
+
* 2. Base Token (3 days TTL) - generated from API Token automatically
|
|
8
|
+
*/
|
|
9
|
+
import { getConfig } from '../../commands/config.js';
|
|
10
|
+
export class SeaTableClient {
|
|
11
|
+
serverUrl;
|
|
12
|
+
apiToken;
|
|
13
|
+
baseToken = null;
|
|
14
|
+
baseTokenExpiry = null;
|
|
15
|
+
dtableServer = null;
|
|
16
|
+
dtableUuid = null;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.apiToken = config.apiToken;
|
|
19
|
+
this.serverUrl = config.serverUrl || 'https://cloud.seatable.io';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create client from Husky config
|
|
23
|
+
*/
|
|
24
|
+
static fromConfig() {
|
|
25
|
+
const config = getConfig();
|
|
26
|
+
const seaTableConfig = {
|
|
27
|
+
apiToken: config.seatableApiToken || process.env.SEATABLE_API_TOKEN || '',
|
|
28
|
+
serverUrl: config.seatableServerUrl || process.env.SEATABLE_SERVER_URL || 'https://cloud.seatable.io',
|
|
29
|
+
};
|
|
30
|
+
if (!seaTableConfig.apiToken) {
|
|
31
|
+
throw new Error('Missing SeaTable API Token. Configure with:\n' +
|
|
32
|
+
' husky config set seatable-api-token <token>\n\n' +
|
|
33
|
+
'Get your token from SeaTable: Base → Advanced → API Token');
|
|
34
|
+
}
|
|
35
|
+
return new SeaTableClient(seaTableConfig);
|
|
36
|
+
}
|
|
37
|
+
isApiGateway() {
|
|
38
|
+
return (this.dtableServer || '').includes('api-gateway');
|
|
39
|
+
}
|
|
40
|
+
buildEndpoint(path) {
|
|
41
|
+
const base = this.isApiGateway()
|
|
42
|
+
? '/api/v2/dtables/'
|
|
43
|
+
: '/dtable-server/api/v1/dtables/';
|
|
44
|
+
return `${base}${this.dtableUuid || ''}${path}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get a Base Token from the API Token
|
|
48
|
+
* Base Tokens are valid for 3 days
|
|
49
|
+
*/
|
|
50
|
+
async getBaseToken() {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
if (this.baseToken && this.baseTokenExpiry && now < this.baseTokenExpiry) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const response = await fetch(`${this.serverUrl}/api/v2.1/dtable/app-access-token/`, {
|
|
56
|
+
method: 'GET',
|
|
57
|
+
headers: {
|
|
58
|
+
'Authorization': `Token ${this.apiToken}`,
|
|
59
|
+
'Accept': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const error = await response.text();
|
|
64
|
+
throw new Error(`Failed to get SeaTable Base Token: ${response.status} ${error}`);
|
|
65
|
+
}
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
this.baseToken = data.access_token;
|
|
68
|
+
this.dtableServer = data.dtable_server;
|
|
69
|
+
this.dtableUuid = data.dtable_uuid;
|
|
70
|
+
// Set expiry to 2.5 days from now (to be safe)
|
|
71
|
+
this.baseTokenExpiry = now + (2.5 * 24 * 60 * 60 * 1000);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Make an authenticated API request
|
|
75
|
+
*/
|
|
76
|
+
async request(path, options = {}) {
|
|
77
|
+
await this.getBaseToken();
|
|
78
|
+
if (!this.dtableServer || !this.baseToken || !this.dtableUuid) {
|
|
79
|
+
throw new Error('Failed to obtain SeaTable Base Token');
|
|
80
|
+
}
|
|
81
|
+
const endpoint = this.buildEndpoint(path);
|
|
82
|
+
const baseUrl = (this.dtableServer || '').replace(/\/+$/, '');
|
|
83
|
+
const url = `${baseUrl}${endpoint}`;
|
|
84
|
+
const response = await fetch(url, {
|
|
85
|
+
...options,
|
|
86
|
+
headers: {
|
|
87
|
+
'Authorization': `Bearer ${this.baseToken}`,
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
...options.headers,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.text();
|
|
94
|
+
throw new Error(`SeaTable API Error ${response.status}: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
return response.json();
|
|
97
|
+
}
|
|
98
|
+
// =========================================================================
|
|
99
|
+
// METADATA
|
|
100
|
+
// =========================================================================
|
|
101
|
+
async getMetadata() {
|
|
102
|
+
const response = await this.request('/metadata/');
|
|
103
|
+
// Response may be { metadata: { tables: [...] } } or directly { tables: [...] }
|
|
104
|
+
if ('metadata' in response && response.metadata) {
|
|
105
|
+
return response.metadata;
|
|
106
|
+
}
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
// =========================================================================
|
|
110
|
+
// ROWS
|
|
111
|
+
// =========================================================================
|
|
112
|
+
async listRows(params) {
|
|
113
|
+
const query = new URLSearchParams({
|
|
114
|
+
table_name: params.table_name,
|
|
115
|
+
});
|
|
116
|
+
if (params.view_name)
|
|
117
|
+
query.set('view_name', params.view_name);
|
|
118
|
+
if (params.order_by)
|
|
119
|
+
query.set('order_by', params.order_by);
|
|
120
|
+
if (params.direction)
|
|
121
|
+
query.set('direction', params.direction);
|
|
122
|
+
if (params.start !== undefined)
|
|
123
|
+
query.set('start', String(params.start));
|
|
124
|
+
if (params.limit !== undefined)
|
|
125
|
+
query.set('limit', String(params.limit));
|
|
126
|
+
const response = await this.request(`/rows/?${query}`);
|
|
127
|
+
return response.rows;
|
|
128
|
+
}
|
|
129
|
+
async getRow(tableName, rowId) {
|
|
130
|
+
try {
|
|
131
|
+
const query = new URLSearchParams({
|
|
132
|
+
table_name: tableName,
|
|
133
|
+
row_id: rowId,
|
|
134
|
+
});
|
|
135
|
+
return await this.request(`/rows/?${query}`);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async queryRows(tableName, filters, params) {
|
|
142
|
+
const body = {
|
|
143
|
+
table_name: tableName,
|
|
144
|
+
filters: filters.filters || [],
|
|
145
|
+
filter_conjunction: filters.filter_conjunction || 'And',
|
|
146
|
+
...params,
|
|
147
|
+
};
|
|
148
|
+
return this.request('/filtered-rows/', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async searchRows(tableName, searchQuery) {
|
|
154
|
+
const query = new URLSearchParams({
|
|
155
|
+
table_name: tableName,
|
|
156
|
+
q: searchQuery,
|
|
157
|
+
});
|
|
158
|
+
const response = await this.request(`/search/?${query}`);
|
|
159
|
+
return response.rows;
|
|
160
|
+
}
|
|
161
|
+
async appendRow(tableName, row) {
|
|
162
|
+
const response = await this.request('/rows/', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
table_name: tableName,
|
|
166
|
+
rows: [row],
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
if (response.rows?.[0])
|
|
170
|
+
return response.rows[0];
|
|
171
|
+
if (response.first_row)
|
|
172
|
+
return response.first_row;
|
|
173
|
+
let rowId = response.row_ids?.[0];
|
|
174
|
+
if (rowId && typeof rowId === 'object' && '_id' in rowId) {
|
|
175
|
+
const fetchedRow = await this.getRow(tableName, rowId._id);
|
|
176
|
+
if (fetchedRow)
|
|
177
|
+
return fetchedRow;
|
|
178
|
+
}
|
|
179
|
+
throw new Error('Unexpected append response from SeaTable');
|
|
180
|
+
}
|
|
181
|
+
async updateRow(tableName, rowId, data) {
|
|
182
|
+
return this.request('/rows/', {
|
|
183
|
+
method: 'PUT',
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
table_name: tableName,
|
|
186
|
+
row_id: rowId,
|
|
187
|
+
row: data,
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async deleteRow(tableName, rowId) {
|
|
192
|
+
return this.request('/rows/', {
|
|
193
|
+
method: 'DELETE',
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
table_name: tableName,
|
|
196
|
+
row_id: rowId,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async deleteRows(tableName, rowIds) {
|
|
201
|
+
return this.request('/batch-delete-rows/', {
|
|
202
|
+
method: 'DELETE',
|
|
203
|
+
body: JSON.stringify({
|
|
204
|
+
table_name: tableName,
|
|
205
|
+
row_ids: rowIds,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export default SeaTableClient;
|