ticlawk-connector 0.1.0-alpha.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/LICENSE +15 -0
- package/README.md +38 -0
- package/bin/ticlawk-connector.mjs +150 -0
- package/cc-watcher.mjs +229 -0
- package/package.json +53 -0
- package/ticlawk-api.mjs +128 -0
- package/ticlawk-connector.mjs +1117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ticlawk
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# ticlawk-connector
|
|
2
|
+
|
|
3
|
+
Local connector for pairing Claude Code and OpenClaw agents with ticlawk.
|
|
4
|
+
|
|
5
|
+
Status: alpha
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ticlawk-connector start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The connector reads config from `~/.ticlawk/.config`.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx ticlawk-connector start
|
|
19
|
+
npx ticlawk-connector health
|
|
20
|
+
npx ticlawk-connector config-path
|
|
21
|
+
npx ticlawk-connector pair --code 123456 --session-id <claude-session-id>
|
|
22
|
+
npx ticlawk-connector pair --code 123456 --type openclaw --channel-id myskills --name myskills
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Config
|
|
26
|
+
|
|
27
|
+
Example `~/.ticlawk/.config`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
TICLAWK_API_KEY=tk_xxxx
|
|
31
|
+
TICLAWK_API_URL=https://ticlawk.com
|
|
32
|
+
GATEWAY_HOST=127.0.0.1
|
|
33
|
+
GATEWAY_PORT=18789
|
|
34
|
+
CC_COMPACT_THRESHOLD=500000
|
|
35
|
+
FEED_RELAY_PORT=3002
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`TICLAWK_API_KEY` is written automatically after successful pairing.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { request } from 'node:http';
|
|
4
|
+
import { startConnector, TICLAWK_CONFIG_PATH } from '../ticlawk-connector.mjs';
|
|
5
|
+
|
|
6
|
+
function printUsage() {
|
|
7
|
+
console.log(`ticlawk-connector
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
ticlawk-connector start
|
|
11
|
+
ticlawk-connector health
|
|
12
|
+
ticlawk-connector pair --code <code> --session-id <id>
|
|
13
|
+
ticlawk-connector pair --code <code> --type openclaw --channel-id <id> [--name <name>]
|
|
14
|
+
ticlawk-connector config-path
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const args = { _: [] };
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const arg = argv[i];
|
|
22
|
+
if (arg.startsWith('--')) {
|
|
23
|
+
const key = arg.slice(2);
|
|
24
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
|
|
25
|
+
args[key] = value;
|
|
26
|
+
} else {
|
|
27
|
+
args._.push(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function postLocal(path, body) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const req = request(
|
|
36
|
+
{
|
|
37
|
+
host: '127.0.0.1',
|
|
38
|
+
port: 3002,
|
|
39
|
+
path,
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
},
|
|
43
|
+
(res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
resolve({
|
|
49
|
+
statusCode: res.statusCode || 0,
|
|
50
|
+
body: data ? JSON.parse(data) : {},
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
reject(err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
req.on('error', reject);
|
|
59
|
+
req.write(JSON.stringify(body));
|
|
60
|
+
req.end();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getLocal(path) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const req = request(
|
|
67
|
+
{ host: '127.0.0.1', port: 3002, path, method: 'GET' },
|
|
68
|
+
(res) => {
|
|
69
|
+
let data = '';
|
|
70
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
71
|
+
res.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
resolve({
|
|
74
|
+
statusCode: res.statusCode || 0,
|
|
75
|
+
body: data ? JSON.parse(data) : {},
|
|
76
|
+
});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
reject(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
req.on('error', reject);
|
|
84
|
+
req.end();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function main() {
|
|
89
|
+
const args = parseArgs(process.argv.slice(2));
|
|
90
|
+
const command = args._[0] || 'start';
|
|
91
|
+
|
|
92
|
+
if (command === 'start') {
|
|
93
|
+
await startConnector();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (command === 'config-path') {
|
|
98
|
+
console.log(TICLAWK_CONFIG_PATH);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (command === 'health') {
|
|
103
|
+
const res = await getLocal('/health');
|
|
104
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
105
|
+
process.exitCode = res.statusCode >= 400 ? 1 : 0;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'pair') {
|
|
110
|
+
if (!args.code) {
|
|
111
|
+
console.error('--code is required');
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const serviceType = args.type === 'openclaw' ? 'openclaw' : 'claude_code';
|
|
116
|
+
const payload = serviceType === 'openclaw'
|
|
117
|
+
? {
|
|
118
|
+
code: args.code,
|
|
119
|
+
serviceType,
|
|
120
|
+
channelId: args['channel-id'],
|
|
121
|
+
name: args.name || args['channel-id'],
|
|
122
|
+
}
|
|
123
|
+
: {
|
|
124
|
+
code: args.code,
|
|
125
|
+
sessionId: args['session-id'],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (serviceType === 'openclaw' && !payload.channelId) {
|
|
129
|
+
console.error('--channel-id is required for openclaw pairing');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (serviceType === 'claude_code' && !payload.sessionId) {
|
|
133
|
+
console.error('--session-id is required for claude_code pairing');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const res = await postLocal('/pair', payload);
|
|
138
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
139
|
+
process.exitCode = res.statusCode >= 400 ? 1 : 0;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
printUsage();
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
main().catch((err) => {
|
|
148
|
+
console.error(err.message || String(err));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
package/cc-watcher.mjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-watcher.mjs
|
|
3
|
+
*
|
|
4
|
+
* Watches Claude Code sessions for new output and exposes transcript helpers.
|
|
5
|
+
* Ticlawk connector uses this to discover sessions, sync transcript history,
|
|
6
|
+
* and detect new transcript entries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, statSync, watch } from 'node:fs';
|
|
10
|
+
import { readdir } from 'node:fs/promises';
|
|
11
|
+
import { join, basename } from 'node:path';
|
|
12
|
+
|
|
13
|
+
// ── Discover Sessions ───────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan a .claude/projects/ directory for session .jsonl files.
|
|
17
|
+
* Returns an array of { sessionId, project, path, projectDir }
|
|
18
|
+
*/
|
|
19
|
+
export async function discoverSessions(claudeProjectsDir) {
|
|
20
|
+
const sessions = [];
|
|
21
|
+
|
|
22
|
+
let projectDirs;
|
|
23
|
+
try {
|
|
24
|
+
projectDirs = await readdir(claudeProjectsDir);
|
|
25
|
+
} catch {
|
|
26
|
+
return sessions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const projName of projectDirs) {
|
|
30
|
+
const projPath = join(claudeProjectsDir, projName);
|
|
31
|
+
|
|
32
|
+
let files;
|
|
33
|
+
try {
|
|
34
|
+
files = await readdir(projPath);
|
|
35
|
+
} catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Collect all .jsonl sessions in this project, pick the most recent one
|
|
40
|
+
let best = null;
|
|
41
|
+
let bestMtime = 0;
|
|
42
|
+
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
45
|
+
if (file.includes('subagent')) continue;
|
|
46
|
+
|
|
47
|
+
const filePath = join(projPath, file);
|
|
48
|
+
let mtime = 0;
|
|
49
|
+
try {
|
|
50
|
+
mtime = statSync(filePath).mtimeMs;
|
|
51
|
+
} catch { continue; }
|
|
52
|
+
|
|
53
|
+
if (mtime > bestMtime) {
|
|
54
|
+
bestMtime = mtime;
|
|
55
|
+
best = { file, filePath };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!best) continue;
|
|
60
|
+
|
|
61
|
+
const sessionId = basename(best.file, '.jsonl');
|
|
62
|
+
|
|
63
|
+
// Read cwd directly from transcript (first user entry has it)
|
|
64
|
+
let projectDir = null;
|
|
65
|
+
try {
|
|
66
|
+
const content = readFileSync(best.filePath, 'utf8');
|
|
67
|
+
for (const line of content.split('\n')) {
|
|
68
|
+
if (!line.trim()) continue;
|
|
69
|
+
const entry = JSON.parse(line);
|
|
70
|
+
if (entry.cwd) {
|
|
71
|
+
projectDir = entry.cwd;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch { /* ignore */ }
|
|
76
|
+
|
|
77
|
+
if (!projectDir) {
|
|
78
|
+
// Fallback: naive decode
|
|
79
|
+
projectDir = '/' + projName.replace(/^-/, '').replace(/-/g, '/');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
sessions.push({
|
|
83
|
+
sessionId,
|
|
84
|
+
project: projName,
|
|
85
|
+
path: best.filePath,
|
|
86
|
+
projectDir,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return sessions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Watch Transcript ────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Watch a transcript .jsonl file for new assistant messages.
|
|
97
|
+
* Calls onMessage(msg) when a new assistant message is detected.
|
|
98
|
+
* msg = { type: 'assistant', text: string, raw: object }
|
|
99
|
+
*
|
|
100
|
+
* Returns a cleanup function to stop watching.
|
|
101
|
+
*/
|
|
102
|
+
export function watchTranscript(transcriptPath, onMessage) {
|
|
103
|
+
let lastSize = 0;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
lastSize = statSync(transcriptPath).size;
|
|
107
|
+
} catch {
|
|
108
|
+
// File might not exist yet
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const checkForNew = () => {
|
|
112
|
+
let currentSize;
|
|
113
|
+
try {
|
|
114
|
+
currentSize = statSync(transcriptPath).size;
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (currentSize <= lastSize) return;
|
|
120
|
+
|
|
121
|
+
// Read only the new bytes (use Buffer to slice by byte offset, not char offset)
|
|
122
|
+
const buf = readFileSync(transcriptPath);
|
|
123
|
+
const newContent = buf.subarray(lastSize).toString('utf8');
|
|
124
|
+
lastSize = currentSize;
|
|
125
|
+
|
|
126
|
+
// Parse new lines
|
|
127
|
+
const lines = newContent.split('\n').filter(l => l.trim());
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
let entry;
|
|
130
|
+
try {
|
|
131
|
+
entry = JSON.parse(line);
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (entry.type !== 'assistant' && entry.type !== 'user') continue;
|
|
137
|
+
|
|
138
|
+
// Extract text and tool info from the message content
|
|
139
|
+
const content = entry.message?.content;
|
|
140
|
+
let text = '';
|
|
141
|
+
let toolNames = [];
|
|
142
|
+
if (typeof content === 'string') {
|
|
143
|
+
text = content;
|
|
144
|
+
} else if (Array.isArray(content)) {
|
|
145
|
+
text = content
|
|
146
|
+
.filter(c => c.type === 'text')
|
|
147
|
+
.map(c => c.text)
|
|
148
|
+
.join('\n');
|
|
149
|
+
toolNames = content
|
|
150
|
+
.filter(c => c.type === 'tool_use')
|
|
151
|
+
.map(c => c.name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stopReason = entry.message?.stop_reason;
|
|
155
|
+
|
|
156
|
+
// Emit tool_use events (agent is processing)
|
|
157
|
+
if (entry.type === 'assistant' && toolNames.length > 0 && stopReason === 'tool_use') {
|
|
158
|
+
onMessage({ type: 'tool_use', text: '', toolNames, raw: entry });
|
|
159
|
+
continue; // Don't also emit as text message
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!text) continue;
|
|
163
|
+
|
|
164
|
+
onMessage({ type: entry.type, text, raw: entry });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Use fs.watch for instant detection, poll as fallback
|
|
169
|
+
let watcher;
|
|
170
|
+
try {
|
|
171
|
+
watcher = watch(transcriptPath, { persistent: false }, () => {
|
|
172
|
+
checkForNew();
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
// fs.watch not available, fall back to polling
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const interval = setInterval(checkForNew, 2000);
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
clearInterval(interval);
|
|
182
|
+
watcher?.close();
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Parse Transcript ────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extract text from a transcript entry's message content.
|
|
190
|
+
*/
|
|
191
|
+
function extractText(entry) {
|
|
192
|
+
const content = entry.message?.content;
|
|
193
|
+
if (typeof content === 'string') return content;
|
|
194
|
+
if (Array.isArray(content)) {
|
|
195
|
+
return content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
|
196
|
+
}
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Read transcript and return all user/assistant messages.
|
|
202
|
+
* Each entry: { type: 'user'|'assistant', text: string, timestamp: number }
|
|
203
|
+
*/
|
|
204
|
+
export function readTranscript(transcriptPath) {
|
|
205
|
+
let content;
|
|
206
|
+
try {
|
|
207
|
+
content = readFileSync(transcriptPath, 'utf8');
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const messages = [];
|
|
213
|
+
for (const line of content.split('\n')) {
|
|
214
|
+
if (!line.trim()) continue;
|
|
215
|
+
let entry;
|
|
216
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
217
|
+
|
|
218
|
+
if (entry.type !== 'user' && entry.type !== 'assistant') continue;
|
|
219
|
+
|
|
220
|
+
const text = extractText(entry);
|
|
221
|
+
if (!text) continue;
|
|
222
|
+
|
|
223
|
+
// Use message timestamp if available, otherwise approximate
|
|
224
|
+
const timestamp = entry.message?.timestamp || entry.timestamp || 0;
|
|
225
|
+
messages.push({ type: entry.type, text, timestamp });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return messages;
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ticlawk-connector",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Local connector for pairing Claude Code and OpenClaw agents with ticlawk.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "ticlawk-connector.mjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ticlawk-connector": "bin/ticlawk-connector.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"cc-watcher.mjs",
|
|
13
|
+
"ticlawk-api.mjs",
|
|
14
|
+
"ticlawk-connector.mjs",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"start": "node ./bin/ticlawk-connector.mjs start",
|
|
26
|
+
"connector": "node ./bin/ticlawk-connector.mjs start",
|
|
27
|
+
"health": "node ./bin/ticlawk-connector.mjs health",
|
|
28
|
+
"dev": "echo 'Configure ~/.ticlawk/.config, then: npm run connector'",
|
|
29
|
+
"generate:setup-docs": "node scripts/generate-setup-docs.mjs"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/darthjaja6/ticlawk.git"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"ticlawk",
|
|
37
|
+
"connector",
|
|
38
|
+
"claude-code",
|
|
39
|
+
"openclaw",
|
|
40
|
+
"agent"
|
|
41
|
+
],
|
|
42
|
+
"author": "ticlawk",
|
|
43
|
+
"license": "ISC",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/darthjaja6/ticlawk/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/darthjaja6/ticlawk#readme",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@supabase/supabase-js": "^2.99.2",
|
|
50
|
+
"openclaw": "^2026.3.13",
|
|
51
|
+
"ws": "^8.19.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/ticlawk-api.mjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticlawk-api.mjs
|
|
3
|
+
*
|
|
4
|
+
* HTTP API client for ticlawk-connector.
|
|
5
|
+
* All operations go through ticlawk.com/api/* (or TICLAWK_API_URL).
|
|
6
|
+
* No direct Supabase access — only needs a tk_ API key.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function getApiUrl() { return process.env.TICLAWK_API_URL || 'https://ticlawk.com'; }
|
|
10
|
+
function getApiKey() { return process.env.TICLAWK_API_KEY || ''; }
|
|
11
|
+
|
|
12
|
+
async function apiFetch(path, opts = {}) {
|
|
13
|
+
const url = `${getApiUrl()}${path}`;
|
|
14
|
+
const res = await fetch(url, {
|
|
15
|
+
...opts,
|
|
16
|
+
headers: {
|
|
17
|
+
'Authorization': `Bearer ${getApiKey()}`,
|
|
18
|
+
...(opts.body && !(opts.body instanceof FormData) ? {'Content-Type': 'application/json'} : {}),
|
|
19
|
+
...opts.headers,
|
|
20
|
+
},
|
|
21
|
+
signal: AbortSignal.timeout(opts.timeout || 15000),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const text = await res.text().catch(() => '');
|
|
26
|
+
let msg;
|
|
27
|
+
try { msg = JSON.parse(text).error; } catch { msg = text; }
|
|
28
|
+
throw new Error(`API ${res.status}: ${msg || res.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Channels ──
|
|
35
|
+
|
|
36
|
+
export async function getChannels() {
|
|
37
|
+
const { data } = await apiFetch('/api/channels');
|
|
38
|
+
return data || [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function getChannel(id) {
|
|
42
|
+
const { data } = await apiFetch(`/api/channels/${id}`);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function updateChannel(id, updates) {
|
|
47
|
+
return apiFetch(`/api/channels/${id}`, {
|
|
48
|
+
method: 'PATCH',
|
|
49
|
+
body: JSON.stringify(updates),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Messages ──
|
|
54
|
+
|
|
55
|
+
export async function getPendingMessages() {
|
|
56
|
+
const { data } = await apiFetch('/api/messages/pending');
|
|
57
|
+
return data || [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function insertMessage(row) {
|
|
61
|
+
const { data } = await apiFetch('/api/messages', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: JSON.stringify(row),
|
|
64
|
+
});
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function syncMessages(rows) {
|
|
69
|
+
return apiFetch('/api/messages/sync', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: JSON.stringify({ rows }),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function updateMessage(id, updates) {
|
|
76
|
+
return apiFetch(`/api/messages/${id}`, {
|
|
77
|
+
method: 'PATCH',
|
|
78
|
+
body: JSON.stringify(updates),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Cards ──
|
|
83
|
+
|
|
84
|
+
export async function insertCard(row) {
|
|
85
|
+
const { data } = await apiFetch('/api/cards', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: JSON.stringify(row),
|
|
88
|
+
});
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Upload ──
|
|
93
|
+
|
|
94
|
+
export async function uploadFile(filePath, fileData, contentType) {
|
|
95
|
+
const formData = new FormData();
|
|
96
|
+
formData.append('file', new Blob([fileData], { type: contentType }), filePath);
|
|
97
|
+
formData.append('path', filePath);
|
|
98
|
+
formData.append('contentType', contentType);
|
|
99
|
+
|
|
100
|
+
const { url } = await apiFetch('/api/upload', {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
body: formData,
|
|
103
|
+
timeout: 30000,
|
|
104
|
+
});
|
|
105
|
+
return url;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Dispatch (existing endpoint) ──
|
|
109
|
+
|
|
110
|
+
export async function dispatch(event) {
|
|
111
|
+
return apiFetch('/dispatch', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: JSON.stringify(event),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Pair (no auth needed) ──
|
|
118
|
+
|
|
119
|
+
export async function pair(payload) {
|
|
120
|
+
const url = `${getApiUrl()}/dispatch`;
|
|
121
|
+
const res = await fetch(url, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ action: 'pair', ...payload }),
|
|
125
|
+
signal: AbortSignal.timeout(15000),
|
|
126
|
+
});
|
|
127
|
+
return res.json();
|
|
128
|
+
}
|