@lightyearminds/dotline-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/index.js +218 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Dotline Agent (CLI)
|
|
2
|
+
|
|
3
|
+
The Dotline backend cannot reach `localhost` or private network addresses from the cloud.
|
|
4
|
+
Dotline Agent runs on the user's machine and **executes HTTP requests locally**, then
|
|
5
|
+
returns the response back to Dotline.
|
|
6
|
+
|
|
7
|
+
## Install (dev)
|
|
8
|
+
|
|
9
|
+
From the repo root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd agent-cli
|
|
13
|
+
npm install
|
|
14
|
+
npm run build
|
|
15
|
+
node dist/index.js --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Pair (from the UI)
|
|
19
|
+
|
|
20
|
+
1) Your Dotline UI calls `POST /agents/pairing-code` (authenticated) and shows the code.
|
|
21
|
+
|
|
22
|
+
2) On your machine:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
dotline-agent login --api https://<DOTLINE_API> --code <PAIRING_CODE> --name "My Machine"
|
|
26
|
+
dotline-agent start --api https://<DOTLINE_API>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## What it supports
|
|
30
|
+
|
|
31
|
+
* JSON/text requests (GET/POST/PUT/PATCH/DELETE)
|
|
32
|
+
* Localhost/private IP targets
|
|
33
|
+
|
|
34
|
+
Multipart/FormData support can be added next (it requires streaming files and boundaries).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
function hasTask(r) {
|
|
6
|
+
return typeof r.id === "string";
|
|
7
|
+
}
|
|
8
|
+
const CONFIG_DIR = path.join(os.homedir(), ".dotline");
|
|
9
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "agent.json");
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
Dotline Agent
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
dotline-agent login --api <DOTLINE_API_BASE> --code <PAIRING_CODE> [--name "My Machine"]
|
|
16
|
+
dotline-agent start --api <DOTLINE_API_BASE>
|
|
17
|
+
dotline-agent status
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
dotline-agent login --api https://api.dotline.app --code ABCDEF1234 --name "MacBook"
|
|
21
|
+
dotline-agent start --api https://api.dotline.app
|
|
22
|
+
`);
|
|
23
|
+
}
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (!a)
|
|
29
|
+
continue;
|
|
30
|
+
if (a.startsWith("--")) {
|
|
31
|
+
const k = a.slice(2);
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
if (next && !next.startsWith("--")) {
|
|
34
|
+
out[k] = next;
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
out[k] = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function loadConfig() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function saveConfig(cfg) {
|
|
54
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
55
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), "utf8");
|
|
56
|
+
}
|
|
57
|
+
function normalizeApiBase(api) {
|
|
58
|
+
return api.replace(/\/$/, "");
|
|
59
|
+
}
|
|
60
|
+
async function httpJson(url, init) {
|
|
61
|
+
const res = await fetch(url, init);
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
let json = null;
|
|
64
|
+
try {
|
|
65
|
+
json = JSON.parse(text);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const msg = json?.message || text || `${res.status} ${res.statusText}`;
|
|
72
|
+
throw new Error(msg);
|
|
73
|
+
}
|
|
74
|
+
return json;
|
|
75
|
+
}
|
|
76
|
+
async function cmdLogin(args) {
|
|
77
|
+
const api = args.api;
|
|
78
|
+
const code = args.code;
|
|
79
|
+
const name = args.name ?? "My Machine";
|
|
80
|
+
if (!api || !code) {
|
|
81
|
+
usage();
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const apiBase = normalizeApiBase(api);
|
|
85
|
+
const resp = (await httpJson(`${apiBase}/agents/claim`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/json" },
|
|
88
|
+
body: JSON.stringify({ code, name }),
|
|
89
|
+
}));
|
|
90
|
+
saveConfig({ api: apiBase, token: resp.agentToken, agentId: resp.agentId });
|
|
91
|
+
console.log(`✅ Agent paired as "${resp.name}" (agentId=${resp.agentId}).`);
|
|
92
|
+
console.log(`Config saved to ${CONFIG_PATH}`);
|
|
93
|
+
}
|
|
94
|
+
async function cmdStatus() {
|
|
95
|
+
const cfg = loadConfig();
|
|
96
|
+
if (!cfg.api || !cfg.token) {
|
|
97
|
+
console.log("Not logged in. Run: dotline-agent login --api <...> --code <...>");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const me = await httpJson(`${cfg.api}/agents/me`, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: { "x-dotline-agent-token": cfg.token },
|
|
104
|
+
});
|
|
105
|
+
console.log(JSON.stringify(me, null, 2));
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.error(`❌ ${e instanceof Error ? e.message : String(e)}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function runHttpTask(api, token, task) {
|
|
113
|
+
const req = task.request ?? {};
|
|
114
|
+
const url = String(req.url ?? "");
|
|
115
|
+
const method = String(req.method ?? "GET").toUpperCase();
|
|
116
|
+
const headers = (req.headers ?? {});
|
|
117
|
+
const bodyText = req.bodyText;
|
|
118
|
+
const started = Date.now();
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method,
|
|
122
|
+
headers,
|
|
123
|
+
body: bodyText === undefined ? undefined : String(bodyText),
|
|
124
|
+
});
|
|
125
|
+
const text = await res.text();
|
|
126
|
+
const elapsedMs = Date.now() - started;
|
|
127
|
+
await httpJson(`${api}/agents/tasks/${task.id}/complete`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
"x-dotline-agent-token": token,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
statusCode: res.status,
|
|
135
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
136
|
+
bodyText: text,
|
|
137
|
+
elapsedMs,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
const elapsedMs = Date.now() - started;
|
|
143
|
+
await httpJson(`${api}/agents/tasks/${task.id}/complete`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"x-dotline-agent-token": token,
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
error: e instanceof Error ? e.message : String(e),
|
|
151
|
+
elapsedMs,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function cmdStart(args) {
|
|
157
|
+
const cfg = loadConfig();
|
|
158
|
+
const api = normalizeApiBase(args.api ?? cfg.api ?? "");
|
|
159
|
+
const token = cfg.token ?? undefined;
|
|
160
|
+
if (!api || !token) {
|
|
161
|
+
console.log("Not logged in. Run: dotline-agent login --api <...> --code <...>");
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
console.log(`🤖 Dotline Agent started. Polling ${api} ...`);
|
|
165
|
+
while (true) {
|
|
166
|
+
try {
|
|
167
|
+
// heartbeat (best effort)
|
|
168
|
+
fetch(`${api}/agents/heartbeat`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "x-dotline-agent-token": token },
|
|
171
|
+
}).catch(() => void 0);
|
|
172
|
+
const next = (await httpJson(`${api}/agents/tasks/next?waitMs=25000`, {
|
|
173
|
+
method: "GET",
|
|
174
|
+
headers: { "x-dotline-agent-token": token },
|
|
175
|
+
}));
|
|
176
|
+
if (!hasTask(next)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (next.kind === "HTTP" || next.kind === "HTTP_REQUEST") {
|
|
180
|
+
await runHttpTask(api, token, next);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await httpJson(`${api}/agents/tasks/${next.id}/complete`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
"x-dotline-agent-token": token,
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify({ error: `Unknown task kind: ${next.kind}` }),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
console.error(`⚠️ ${e instanceof Error ? e.message : String(e)}`);
|
|
195
|
+
await sleep(1000);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function main() {
|
|
200
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
201
|
+
if (!cmd) {
|
|
202
|
+
usage();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const args = parseArgs(rest);
|
|
206
|
+
if (cmd === "login")
|
|
207
|
+
return cmdLogin(args);
|
|
208
|
+
if (cmd === "start")
|
|
209
|
+
return cmdStart(args);
|
|
210
|
+
if (cmd === "status")
|
|
211
|
+
return cmdStatus();
|
|
212
|
+
usage();
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
main().catch((e) => {
|
|
216
|
+
console.error(`❌ ${e instanceof Error ? e.message : String(e)}`);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lightyearminds/dotline-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dotline-agent": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.6.3"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
}
|
|
22
|
+
}
|