ticlawk 0.1.12-dev.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 +426 -0
- package/agent-freeway.mjs +2 -0
- package/assets/ticlawk-concept.svg +137 -0
- package/bin/agent-freeway.mjs +4 -0
- package/bin/ticlawk.mjs +594 -0
- package/cc-watcher.mjs +3 -0
- package/package.json +72 -0
- package/scripts/postinstall.mjs +61 -0
- package/src/adapters/telegram/index.mjs +359 -0
- package/src/adapters/ticlawk/api.mjs +360 -0
- package/src/adapters/ticlawk/cards.mjs +149 -0
- package/src/adapters/ticlawk/credentials.mjs +25 -0
- package/src/adapters/ticlawk/index.mjs +1229 -0
- package/src/adapters/ticlawk/wake-client.mjs +204 -0
- package/src/core/adapter-registry.mjs +50 -0
- package/src/core/argv.mjs +38 -0
- package/src/core/bindings/store.mjs +81 -0
- package/src/core/bus.mjs +91 -0
- package/src/core/config.mjs +203 -0
- package/src/core/daemon-install.mjs +246 -0
- package/src/core/diagnostics.mjs +79 -0
- package/src/core/events/worker-events.mjs +80 -0
- package/src/core/executables.mjs +106 -0
- package/src/core/host-id.mjs +48 -0
- package/src/core/http.mjs +65 -0
- package/src/core/logger.mjs +34 -0
- package/src/core/media/inbound.mjs +127 -0
- package/src/core/media/outbound.mjs +163 -0
- package/src/core/profiles.mjs +173 -0
- package/src/core/runtime-contract.mjs +68 -0
- package/src/core/runtime-env.mjs +9 -0
- package/src/core/runtime-registry.mjs +93 -0
- package/src/core/runtime-support.mjs +197 -0
- package/src/core/setup-readiness.mjs +86 -0
- package/src/core/store/json-file-store.mjs +47 -0
- package/src/core/ticlawk-control.mjs +92 -0
- package/src/core/uninstall.mjs +142 -0
- package/src/core/update-state.mjs +62 -0
- package/src/core/update.mjs +178 -0
- package/src/runtimes/claude-code/index.mjs +363 -0
- package/src/runtimes/claude-code/session.mjs +388 -0
- package/src/runtimes/claude-code/transcripts.mjs +206 -0
- package/src/runtimes/codex/index.mjs +306 -0
- package/src/runtimes/codex/session.mjs +750 -0
- package/src/runtimes/openclaw/gateway.mjs +269 -0
- package/src/runtimes/openclaw/identity.mjs +34 -0
- package/src/runtimes/openclaw/index.mjs +228 -0
- package/src/runtimes/openclaw/inflight.mjs +46 -0
- package/src/runtimes/openclaw/target.mjs +57 -0
- package/src/runtimes/opencode/index.mjs +318 -0
- package/src/runtimes/opencode/session.mjs +413 -0
- package/src/runtimes/pi/index.mjs +287 -0
- package/src/runtimes/pi/session.mjs +423 -0
- package/ticlawk.mjs +260 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { getStreamingMode } from './config.mjs';
|
|
2
|
+
const ERROR_MAX_CHARS = 500;
|
|
3
|
+
const DEFAULT_DELTA_FLUSH_MS = 250;
|
|
4
|
+
const DEFAULT_DELTA_FLUSH_CHARS = 64;
|
|
5
|
+
|
|
6
|
+
function truncateError(text) {
|
|
7
|
+
if (!text) return null;
|
|
8
|
+
const trimmed = String(text).trim();
|
|
9
|
+
if (trimmed.length <= ERROR_MAX_CHARS) return trimmed;
|
|
10
|
+
return `${trimmed.slice(0, ERROR_MAX_CHARS)}…`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function runtimeContextLines(binding, info = {}) {
|
|
14
|
+
const meta = binding?.runtimeMeta || {};
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (meta.sessionId) lines.push(`Session: ${meta.sessionId}`);
|
|
17
|
+
if (info.turnId) lines.push(`Turn: ${info.turnId}`);
|
|
18
|
+
if (meta.cwd || meta.workdir) lines.push(`Workdir: ${meta.cwd || meta.workdir}`);
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isRuntimeGatewayFailure(info) {
|
|
23
|
+
return info?.kind === 'gateway-error';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildCodexGatewayFailureText({ binding, info, seconds }) {
|
|
27
|
+
const details = [
|
|
28
|
+
truncateError(info?.errorMessage),
|
|
29
|
+
info?.additionalDetails ? `Details: ${truncateError(info.additionalDetails)}` : null,
|
|
30
|
+
].filter(Boolean);
|
|
31
|
+
const context = runtimeContextLines(binding, info);
|
|
32
|
+
return [
|
|
33
|
+
`⚠️ Codex returned an app-server error after ${seconds}s.`,
|
|
34
|
+
details.length ? details.join('\n') : null,
|
|
35
|
+
'Your message reached ticlawk, but Codex failed while running this session. Reconnect or reset this Codex agent session, then resend your message.',
|
|
36
|
+
context.length ? context.join('\n') : null,
|
|
37
|
+
].filter(Boolean).join('\n\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function shouldStreamRuntime(runtimeName, runtime) {
|
|
41
|
+
return Boolean(runtime?.runTurnStream) && getStreamingMode(runtimeName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function sendAdapterMessage(adapter, binding, payload) {
|
|
45
|
+
await adapter.send(binding, payload);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function sendResult(adapter, binding, inbound, result) {
|
|
49
|
+
if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
|
|
50
|
+
await sendAdapterMessage(adapter, binding, {
|
|
51
|
+
type: 'assistant',
|
|
52
|
+
text: result.text || '',
|
|
53
|
+
media: result.media || [],
|
|
54
|
+
turnId: inbound.messageId || result?.turnId || null,
|
|
55
|
+
replyToMessageId: inbound.messageId || null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
|
|
60
|
+
if (!info || info.ok) return;
|
|
61
|
+
const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
|
|
62
|
+
if (runtimeName === 'Codex' && isRuntimeGatewayFailure(info)) {
|
|
63
|
+
await sendAdapterMessage(adapter, binding, {
|
|
64
|
+
type: 'assistant',
|
|
65
|
+
text: buildCodexGatewayFailureText({ binding, info, seconds }),
|
|
66
|
+
media: [],
|
|
67
|
+
turnId: inbound?.messageId || null,
|
|
68
|
+
replyToMessageId: inbound?.messageId || null,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let summary;
|
|
74
|
+
if (info.kind === 'killed') {
|
|
75
|
+
summary = `⚠️ ${runtimeName} was killed by signal ${info.signal} after ${seconds}s.`;
|
|
76
|
+
} else if (info.kind === 'spawn-failed') {
|
|
77
|
+
summary = `⚠️ ${runtimeName} failed to spawn: ${info.error || 'unknown error'}.`;
|
|
78
|
+
} else if (info.kind === 'gateway-timeout') {
|
|
79
|
+
summary = `⚠️ ${runtimeName} did not respond after ${seconds}s (timeout).`;
|
|
80
|
+
} else if (info.kind === 'gateway-error') {
|
|
81
|
+
summary = `⚠️ ${runtimeName} returned an error after ${seconds}s.`;
|
|
82
|
+
} else if (typeof info.code === 'number') {
|
|
83
|
+
summary = `⚠️ ${runtimeName} exited with code ${info.code} after ${seconds}s.`;
|
|
84
|
+
} else {
|
|
85
|
+
summary = `⚠️ ${runtimeName} failed after ${seconds}s.`;
|
|
86
|
+
}
|
|
87
|
+
const detail = truncateError(info.errorMessage);
|
|
88
|
+
const text = detail ? `${summary}\n\n${detail}` : `${summary}\n\nCheck ~/.ticlawk/ticlawk.log on the host for details.`;
|
|
89
|
+
await sendAdapterMessage(adapter, binding, {
|
|
90
|
+
type: 'assistant',
|
|
91
|
+
text,
|
|
92
|
+
media: [],
|
|
93
|
+
turnId: inbound?.messageId || null,
|
|
94
|
+
replyToMessageId: inbound?.messageId || null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function terminalRuntimeFailure(reason = 'runtime failure') {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
terminal: true,
|
|
102
|
+
reason,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isTerminalRuntimeFailure(result) {
|
|
107
|
+
return Boolean(result && typeof result === 'object' && result.ok === false && result.terminal === true);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function updateBindingRuntimeMeta(ctx, binding, runtimeMetaPatch, extra = {}) {
|
|
111
|
+
return ctx.upsertBinding({
|
|
112
|
+
...binding,
|
|
113
|
+
runtimeMeta: {
|
|
114
|
+
...binding.runtimeMeta,
|
|
115
|
+
...runtimeMetaPatch,
|
|
116
|
+
},
|
|
117
|
+
...extra,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createDeltaAggregator({
|
|
122
|
+
flushDelta,
|
|
123
|
+
flushMs = DEFAULT_DELTA_FLUSH_MS,
|
|
124
|
+
flushChars = DEFAULT_DELTA_FLUSH_CHARS,
|
|
125
|
+
}) {
|
|
126
|
+
let buffer = '';
|
|
127
|
+
let pendingMeta = null;
|
|
128
|
+
let timer = null;
|
|
129
|
+
let flushChain = Promise.resolve();
|
|
130
|
+
|
|
131
|
+
const clearTimer = () => {
|
|
132
|
+
if (!timer) return;
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
timer = null;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const startFlush = () => {
|
|
138
|
+
if (!buffer || typeof flushDelta !== 'function') return flushChain;
|
|
139
|
+
const text = buffer;
|
|
140
|
+
const meta = pendingMeta || {};
|
|
141
|
+
buffer = '';
|
|
142
|
+
pendingMeta = null;
|
|
143
|
+
clearTimer();
|
|
144
|
+
flushChain = flushChain
|
|
145
|
+
.catch(() => {})
|
|
146
|
+
.then(() => flushDelta({ text, ...meta }));
|
|
147
|
+
return flushChain;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const scheduleFlush = () => {
|
|
151
|
+
if (!buffer || timer) return;
|
|
152
|
+
timer = setTimeout(() => {
|
|
153
|
+
timer = null;
|
|
154
|
+
void startFlush();
|
|
155
|
+
}, flushMs);
|
|
156
|
+
timer.unref?.();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
push(text, meta = {}) {
|
|
161
|
+
const normalized = typeof text === 'string' ? text : '';
|
|
162
|
+
if (!normalized) return;
|
|
163
|
+
|
|
164
|
+
const nextMeta = {
|
|
165
|
+
sessionId: meta.sessionId || null,
|
|
166
|
+
turnId: meta.turnId || null,
|
|
167
|
+
cwd: meta.cwd || '',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const sameContext = !pendingMeta
|
|
171
|
+
|| (
|
|
172
|
+
pendingMeta.sessionId === nextMeta.sessionId
|
|
173
|
+
&& pendingMeta.turnId === nextMeta.turnId
|
|
174
|
+
&& pendingMeta.cwd === nextMeta.cwd
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (!sameContext && buffer) {
|
|
178
|
+
void startFlush();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pendingMeta = nextMeta;
|
|
182
|
+
buffer += normalized;
|
|
183
|
+
|
|
184
|
+
if (buffer.length >= flushChars) {
|
|
185
|
+
void startFlush();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
scheduleFlush();
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async flush() {
|
|
193
|
+
clearTimer();
|
|
194
|
+
await startFlush();
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { AF_LOG_PATH } from './config.mjs';
|
|
5
|
+
import {
|
|
6
|
+
commandExists,
|
|
7
|
+
getCurrentUsername,
|
|
8
|
+
getSystemdLinger,
|
|
9
|
+
getWrapperPath,
|
|
10
|
+
installDaemon,
|
|
11
|
+
} from './daemon-install.mjs';
|
|
12
|
+
|
|
13
|
+
function pathContains(dir) {
|
|
14
|
+
return String(process.env.PATH || '')
|
|
15
|
+
.split(':')
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.includes(dir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printPathActionIfNeeded() {
|
|
21
|
+
if (commandExists('ticlawk')) return false;
|
|
22
|
+
|
|
23
|
+
const wrapperPath = getWrapperPath();
|
|
24
|
+
const npmBinPath = getNpmGlobalTiclawkBin();
|
|
25
|
+
const binDir = existsSync(wrapperPath)
|
|
26
|
+
? dirname(wrapperPath)
|
|
27
|
+
: dirname(npmBinPath || '');
|
|
28
|
+
if (!binDir || binDir === '.') return false;
|
|
29
|
+
if (pathContains(binDir)) return false;
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log('Action required');
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(`${binDir} is not on your PATH.`);
|
|
35
|
+
console.log('Add this to your shell profile, then open a new shell:');
|
|
36
|
+
console.log(` export PATH="${binDir}:$PATH"`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getNpmGlobalTiclawkBin() {
|
|
41
|
+
const result = spawnSync('npm', ['prefix', '-g'], { encoding: 'utf8' });
|
|
42
|
+
const prefix = String(result.stdout || '').trim();
|
|
43
|
+
if (!prefix) return '';
|
|
44
|
+
if (process.platform === 'win32') return `${prefix}\\ticlawk.cmd`;
|
|
45
|
+
return `${prefix}/bin/ticlawk`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printLingerActionIfNeeded() {
|
|
49
|
+
if (process.platform !== 'linux' || !commandExists('loginctl')) return false;
|
|
50
|
+
const username = getCurrentUsername();
|
|
51
|
+
if (getSystemdLinger(username) !== 'no') return false;
|
|
52
|
+
|
|
53
|
+
console.log();
|
|
54
|
+
console.log('Action required');
|
|
55
|
+
console.log();
|
|
56
|
+
console.log('ticlawk may stop when you log out because systemd linger is off.');
|
|
57
|
+
console.log();
|
|
58
|
+
console.log('Run this command to keep it running after logout and reboot:');
|
|
59
|
+
console.log(` sudo loginctl enable-linger ${username}`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printReady() {
|
|
64
|
+
console.log();
|
|
65
|
+
console.log('Ready:');
|
|
66
|
+
console.log(' ticlawk health');
|
|
67
|
+
console.log(`Logs: ${AF_LOG_PATH}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runSetupReadiness({ ensureDaemon = false, showActions = true, showReady = true } = {}) {
|
|
71
|
+
let daemonReady = true;
|
|
72
|
+
if (ensureDaemon) {
|
|
73
|
+
daemonReady = await installDaemon();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hasPathAction = showActions ? printPathActionIfNeeded() : false;
|
|
77
|
+
const hasLingerAction = showActions ? printLingerActionIfNeeded() : false;
|
|
78
|
+
|
|
79
|
+
if (daemonReady && !hasPathAction && !hasLingerAction && showReady) {
|
|
80
|
+
printReady();
|
|
81
|
+
} else if (hasPathAction || hasLingerAction) {
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(`Logs: ${AF_LOG_PATH}`);
|
|
84
|
+
}
|
|
85
|
+
return { daemonReady, hasPathAction, hasLingerAction };
|
|
86
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export class JsonFileStore {
|
|
5
|
+
constructor(filePath, fallbackValue) {
|
|
6
|
+
this.filePath = filePath;
|
|
7
|
+
this.fallbackValue = fallbackValue;
|
|
8
|
+
this._pending = Promise.resolve();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
read() {
|
|
12
|
+
if (!existsSync(this.filePath)) return structuredClone(this.fallbackValue);
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return structuredClone(this.fallbackValue);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async write(nextValue) {
|
|
21
|
+
return this._queue(() => {
|
|
22
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
23
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
24
|
+
writeFileSync(tempPath, `${JSON.stringify(nextValue, null, 2)}\n`, { mode: 0o600 });
|
|
25
|
+
renameSync(tempPath, this.filePath);
|
|
26
|
+
return nextValue;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async update(updater) {
|
|
31
|
+
return this._queue(() => {
|
|
32
|
+
const current = this.read();
|
|
33
|
+
const next = updater(current);
|
|
34
|
+
mkdirSync(dirname(this.filePath), { recursive: true });
|
|
35
|
+
const tempPath = `${this.filePath}.tmp`;
|
|
36
|
+
writeFileSync(tempPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
renameSync(tempPath, this.filePath);
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async _queue(task) {
|
|
43
|
+
const run = this._pending.catch(() => undefined).then(task);
|
|
44
|
+
this._pending = run;
|
|
45
|
+
return run;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Bus } from './bus.mjs';
|
|
2
|
+
import { getConfiguredAdapter } from './config.mjs';
|
|
3
|
+
import { createAdapter } from './adapter-registry.mjs';
|
|
4
|
+
import { getBinding, listBindings, upsertBinding, deleteBinding, findBindingByTarget } from './bindings/store.mjs';
|
|
5
|
+
import { buildRuntimeContext, normalizeServiceType } from './runtime-registry.mjs';
|
|
6
|
+
import * as logger from './logger.mjs';
|
|
7
|
+
import { buildImageMessageFromInbound } from './media/inbound.mjs';
|
|
8
|
+
|
|
9
|
+
function createResolveRuntimeBinding(runtimes) {
|
|
10
|
+
return async (payload) => {
|
|
11
|
+
const requested = normalizeServiceType(payload?.serviceType);
|
|
12
|
+
const runtime = runtimes[requested];
|
|
13
|
+
if (!runtime?.resolveBinding) {
|
|
14
|
+
throw new Error(`runtime does not support binding resolution: ${requested}`);
|
|
15
|
+
}
|
|
16
|
+
return runtime.resolveBinding(payload);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createUpsertBindingWithSync(runtimes, adapter) {
|
|
21
|
+
return async (binding) => {
|
|
22
|
+
const nextBinding = await upsertBinding(binding);
|
|
23
|
+
const runtime = nextBinding?.runtime ? runtimes[nextBinding.runtime] : null;
|
|
24
|
+
if (typeof runtime?.onBindingUpdated === 'function') {
|
|
25
|
+
await runtime.onBindingUpdated(nextBinding, { adapter, logger });
|
|
26
|
+
}
|
|
27
|
+
if (typeof adapter.syncBinding === 'function') {
|
|
28
|
+
await adapter.syncBinding(nextBinding);
|
|
29
|
+
}
|
|
30
|
+
return nextBinding;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createCacheBinding(runtimes, getAdapter) {
|
|
35
|
+
return async (binding) => {
|
|
36
|
+
const nextBinding = await upsertBinding(binding);
|
|
37
|
+
const runtime = nextBinding?.runtime ? runtimes[nextBinding.runtime] : null;
|
|
38
|
+
const adapter = getAdapter();
|
|
39
|
+
if (typeof runtime?.onBindingUpdated === 'function') {
|
|
40
|
+
await runtime.onBindingUpdated(nextBinding, { adapter, logger });
|
|
41
|
+
}
|
|
42
|
+
return nextBinding;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createTiclawkController(adapterId = getConfiguredAdapter()) {
|
|
47
|
+
const { runtimes } = await buildRuntimeContext();
|
|
48
|
+
const resolveRuntimeBinding = createResolveRuntimeBinding(runtimes);
|
|
49
|
+
let adapter;
|
|
50
|
+
const cacheBinding = createCacheBinding(runtimes, () => adapter);
|
|
51
|
+
let syncBinding = async (binding) => {
|
|
52
|
+
if (!adapter) {
|
|
53
|
+
throw new Error('adapter not initialized');
|
|
54
|
+
}
|
|
55
|
+
return upsertBinding(binding);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
adapter = createAdapter(adapterId, {
|
|
59
|
+
bus: new Bus(),
|
|
60
|
+
runtimes,
|
|
61
|
+
getBinding,
|
|
62
|
+
listBindings,
|
|
63
|
+
deleteBinding,
|
|
64
|
+
findBindingByTarget,
|
|
65
|
+
resolveRuntimeBinding,
|
|
66
|
+
cacheBinding: (binding) => cacheBinding(binding),
|
|
67
|
+
upsertBinding: (binding) => syncBinding(binding),
|
|
68
|
+
buildImageMessageFromInbound,
|
|
69
|
+
logger,
|
|
70
|
+
});
|
|
71
|
+
syncBinding = createUpsertBindingWithSync(runtimes, adapter);
|
|
72
|
+
|
|
73
|
+
return { adapter };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runTiclawkConnect(payload) {
|
|
77
|
+
const adapterId = payload?.adapter || getConfiguredAdapter();
|
|
78
|
+
const { adapter } = await createTiclawkController(adapterId);
|
|
79
|
+
if (typeof adapter.connect !== 'function') {
|
|
80
|
+
return {
|
|
81
|
+
statusCode: 405,
|
|
82
|
+
body: {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: `${adapterId} does not support connect`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return adapter.connect({
|
|
89
|
+
...payload,
|
|
90
|
+
adapter: adapterId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service and legacy install cleanup.
|
|
3
|
+
*
|
|
4
|
+
* Removes the user service and legacy managed-install files while preserving
|
|
5
|
+
* ~/.ticlawk by default. The npm package is owned by npm.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, rmSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { AF_HOME } from './config.mjs';
|
|
13
|
+
|
|
14
|
+
function getInstallRoots() {
|
|
15
|
+
const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
16
|
+
return [join(dataHome, 'ticlawk'), join(dataHome, 'agent-freeway')];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getWrapperPaths() {
|
|
20
|
+
return [join(homedir(), '.local', 'bin', 'ticlawk'), join(homedir(), '.local', 'bin', 'agent-freeway')];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function commandExists(command) {
|
|
24
|
+
return spawnSync('command', ['-v', command], { shell: true, stdio: 'ignore' }).status === 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function promptYesNo(question) {
|
|
28
|
+
if (!process.stdin.isTTY) {
|
|
29
|
+
return Promise.resolve(false);
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write(`${question} `);
|
|
32
|
+
return new Promise((resolveAnswer) => {
|
|
33
|
+
process.stdin.setEncoding('utf8');
|
|
34
|
+
const onData = (chunk) => {
|
|
35
|
+
process.stdin.removeListener('data', onData);
|
|
36
|
+
process.stdin.pause();
|
|
37
|
+
const answer = String(chunk).trim().toLowerCase();
|
|
38
|
+
resolveAnswer(answer === '' || answer === 'y' || answer === 'yes');
|
|
39
|
+
};
|
|
40
|
+
process.stdin.resume();
|
|
41
|
+
process.stdin.on('data', onData);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runBestEffort(label, command, args, { warn = false } = {}) {
|
|
46
|
+
if (!commandExists(command)) return false;
|
|
47
|
+
const result = spawnSync(command, args, { stdio: 'ignore' });
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
if (warn) console.error(`warning: ${label} failed`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function removePath(path, label, { recursive = false } = {}) {
|
|
56
|
+
if (!existsSync(path)) {
|
|
57
|
+
console.log(`kept missing ${label}: ${path}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (recursive) {
|
|
61
|
+
rmSync(path, { recursive: true, force: true });
|
|
62
|
+
} else {
|
|
63
|
+
unlinkSync(path);
|
|
64
|
+
}
|
|
65
|
+
console.log(`removed ${label}: ${path}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function uninstallSystemd() {
|
|
69
|
+
if (process.platform !== 'linux' || !commandExists('systemctl')) return;
|
|
70
|
+
for (const service of ['ticlawk', 'agent-freeway']) {
|
|
71
|
+
runBestEffort(`systemd stop ${service}`, 'systemctl', ['--user', 'stop', service]);
|
|
72
|
+
runBestEffort(`systemd disable ${service}`, 'systemctl', ['--user', 'disable', service]);
|
|
73
|
+
removePath(join(homedir(), '.config', 'systemd', 'user', `${service}.service`), 'systemd service');
|
|
74
|
+
}
|
|
75
|
+
runBestEffort('systemd daemon-reload', 'systemctl', ['--user', 'daemon-reload'], { warn: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function uninstallLaunchd() {
|
|
79
|
+
if (process.platform !== 'darwin' || !commandExists('launchctl')) return;
|
|
80
|
+
const uid = String(process.getuid?.() || '');
|
|
81
|
+
for (const label of ['ticlawk', 'agent-freeway', 'com.ticlawk.agent-freeway']) {
|
|
82
|
+
runBestEffort(`launchd unload ${label}`, 'launchctl', ['bootout', `gui/${uid}/${label}`]);
|
|
83
|
+
}
|
|
84
|
+
removePath(join(homedir(), 'Library', 'LaunchAgents', 'ticlawk.plist'), 'launchd plist');
|
|
85
|
+
removePath(join(homedir(), 'Library', 'LaunchAgents', 'agent-freeway.plist'), 'launchd plist');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getUninstallHelp() {
|
|
89
|
+
return `ticlawk uninstall
|
|
90
|
+
|
|
91
|
+
Remove the ticlawk service and legacy install files while preserving local data.
|
|
92
|
+
|
|
93
|
+
Removes:
|
|
94
|
+
- user service: systemd on Linux, launchd on macOS
|
|
95
|
+
- CLI wrappers: ~/.local/bin/ticlawk and legacy ~/.local/bin/agent-freeway
|
|
96
|
+
- legacy managed installs: ~/.local/share/ticlawk and ~/.local/share/agent-freeway
|
|
97
|
+
|
|
98
|
+
Preserves:
|
|
99
|
+
- ~/.ticlawk and legacy ~/.agent-freeway
|
|
100
|
+
|
|
101
|
+
To remove the npm package itself:
|
|
102
|
+
npm uninstall -g ticlawk
|
|
103
|
+
|
|
104
|
+
Usage:
|
|
105
|
+
ticlawk uninstall
|
|
106
|
+
ticlawk uninstall -y
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function runSelfUninstall(skipPrompt) {
|
|
111
|
+
const installRoots = getInstallRoots();
|
|
112
|
+
const wrapperPaths = getWrapperPaths();
|
|
113
|
+
|
|
114
|
+
console.log('ticlawk uninstall');
|
|
115
|
+
console.log('will remove the user service');
|
|
116
|
+
console.log(`will remove legacy install files under: ${installRoots.join(', ')}`);
|
|
117
|
+
console.log(`will remove CLI wrappers: ${wrapperPaths.join(', ')}`);
|
|
118
|
+
console.log(`will preserve data directory: ${AF_HOME}`);
|
|
119
|
+
|
|
120
|
+
if (!skipPrompt) {
|
|
121
|
+
if (!process.stdin.isTTY) {
|
|
122
|
+
console.error('refusing to uninstall non-interactively without -y');
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const ok = await promptYesNo('Uninstall ticlawk? [y/N]');
|
|
126
|
+
if (!ok) {
|
|
127
|
+
console.log('aborted');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
uninstallSystemd();
|
|
133
|
+
uninstallLaunchd();
|
|
134
|
+
for (const wrapperPath of wrapperPaths) {
|
|
135
|
+
removePath(wrapperPath, 'CLI wrapper');
|
|
136
|
+
}
|
|
137
|
+
for (const installRoot of installRoots) {
|
|
138
|
+
removePath(installRoot, 'package install', { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
console.log(`preserved data directory: ${AF_HOME}`);
|
|
141
|
+
console.log('uninstall complete');
|
|
142
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { AF_HOME } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
export const AF_UPDATE_STATE_PATH = join(AF_HOME, 'update-state.json');
|
|
6
|
+
|
|
7
|
+
function readJsonFile(filePath) {
|
|
8
|
+
if (!existsSync(filePath)) return {};
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(filePath, 'utf8')) || {};
|
|
11
|
+
} catch {
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeJsonFile(filePath, value) {
|
|
17
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
18
|
+
const tempPath = `${filePath}.tmp-${process.pid}`;
|
|
19
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
20
|
+
renameSync(tempPath, filePath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readUpdateState() {
|
|
24
|
+
return readJsonFile(AF_UPDATE_STATE_PATH);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function writeUpdateState(updates = {}) {
|
|
28
|
+
const current = readUpdateState();
|
|
29
|
+
const next = {
|
|
30
|
+
...current,
|
|
31
|
+
...updates,
|
|
32
|
+
updatedAt: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
writeJsonFile(AF_UPDATE_STATE_PATH, next);
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function setUpdateRequiredState({ adapter, currentVersion, requiredVersion, autoUpdateStatus, autoUpdateError, lastAutoUpdateAttemptAt }) {
|
|
39
|
+
const updates = {
|
|
40
|
+
adapter,
|
|
41
|
+
updateRequired: true,
|
|
42
|
+
currentAgentFreewayVersion: currentVersion || null,
|
|
43
|
+
requiredAgentFreewayVersion: requiredVersion || null,
|
|
44
|
+
};
|
|
45
|
+
if (autoUpdateStatus) updates.autoUpdateStatus = autoUpdateStatus;
|
|
46
|
+
if (autoUpdateError) updates.autoUpdateError = autoUpdateError;
|
|
47
|
+
if (lastAutoUpdateAttemptAt) updates.lastAutoUpdateAttemptAt = lastAutoUpdateAttemptAt;
|
|
48
|
+
return writeUpdateState(updates);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function clearUpdateRequiredState(adapter) {
|
|
52
|
+
const current = readUpdateState();
|
|
53
|
+
if (!current.updateRequired) return current;
|
|
54
|
+
if (adapter && current.adapter && current.adapter !== adapter) return current;
|
|
55
|
+
return writeUpdateState({
|
|
56
|
+
adapter: adapter || current.adapter || null,
|
|
57
|
+
updateRequired: false,
|
|
58
|
+
autoUpdateStatus: null,
|
|
59
|
+
autoUpdateError: null,
|
|
60
|
+
lastClearedAt: new Date().toISOString(),
|
|
61
|
+
});
|
|
62
|
+
}
|