palmier 0.5.1 → 0.5.3
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 +9 -9
- package/dist/agents/agent-instructions.md +7 -11
- package/dist/agents/agent.d.ts +8 -3
- package/dist/agents/agent.js +7 -1
- package/dist/agents/claude.d.ts +2 -1
- package/dist/agents/claude.js +10 -5
- package/dist/agents/codex.d.ts +2 -1
- package/dist/agents/codex.js +10 -6
- package/dist/agents/copilot.d.ts +2 -1
- package/dist/agents/copilot.js +10 -3
- package/dist/agents/gemini.d.ts +2 -1
- package/dist/agents/gemini.js +11 -7
- package/dist/agents/kimi.d.ts +9 -0
- package/dist/agents/kimi.js +35 -0
- package/dist/agents/openclaw.d.ts +2 -1
- package/dist/agents/openclaw.js +3 -1
- package/dist/agents/qwen.d.ts +9 -0
- package/dist/agents/qwen.js +32 -0
- package/dist/agents/shared-prompt.d.ts +1 -1
- package/dist/agents/shared-prompt.js +7 -3
- package/dist/client-store.d.ts +12 -0
- package/dist/client-store.js +57 -0
- package/dist/commands/clients.d.ts +4 -0
- package/dist/commands/clients.js +27 -0
- package/dist/commands/info.js +5 -5
- package/dist/commands/init.js +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/commands/run.js +21 -8
- package/dist/commands/serve.js +1 -1
- package/dist/events.js +1 -1
- package/dist/index.js +13 -13
- package/dist/rpc-handler.js +13 -6
- package/dist/task.d.ts +13 -3
- package/dist/task.js +39 -7
- package/dist/transports/http-transport.js +30 -13
- package/dist/transports/nats-transport.js +4 -4
- package/dist/types.d.ts +3 -2
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +7 -11
- package/src/agents/agent.ts +16 -4
- package/src/agents/claude.ts +11 -6
- package/src/agents/codex.ts +11 -7
- package/src/agents/copilot.ts +10 -4
- package/src/agents/gemini.ts +12 -8
- package/src/agents/kimi.ts +37 -0
- package/src/agents/openclaw.ts +4 -2
- package/src/agents/qwen.ts +34 -0
- package/src/agents/shared-prompt.ts +7 -3
- package/src/client-store.ts +68 -0
- package/src/commands/clients.ts +29 -0
- package/src/commands/info.ts +5 -5
- package/src/commands/init.ts +1 -1
- package/src/commands/pair.ts +4 -4
- package/src/commands/run.ts +22 -8
- package/src/commands/serve.ts +1 -1
- package/src/events.ts +1 -1
- package/src/index.ts +13 -13
- package/src/rpc-handler.ts +15 -6
- package/src/task.ts +43 -8
- package/src/transports/http-transport.ts +32 -13
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -3
- package/test/agent-instructions.test.ts +48 -0
- package/test/agent-output-parsing.test.ts +12 -0
- package/dist/commands/sessions.d.ts +0 -4
- package/dist/commands/sessions.js +0 -27
- package/dist/session-store.d.ts +0 -12
- package/dist/session-store.js +0 -57
- package/src/commands/sessions.ts +0 -29
- package/src/session-store.ts +0 -68
package/src/task.ts
CHANGED
|
@@ -202,31 +202,66 @@ export function beginStreamingMessage(
|
|
|
202
202
|
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
203
203
|
const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
|
|
204
204
|
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
205
|
-
return new StreamingMessageWriter(filePath
|
|
205
|
+
return new StreamingMessageWriter(filePath);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
export class StreamingMessageWriter {
|
|
209
|
-
private
|
|
210
|
-
constructor(private filePath: string, delimiter: string) {
|
|
211
|
-
this.delimiter = delimiter;
|
|
212
|
-
}
|
|
209
|
+
constructor(private filePath: string) {}
|
|
213
210
|
|
|
214
211
|
/** Append a chunk of content to the current message. */
|
|
215
212
|
write(chunk: string): void {
|
|
216
213
|
fs.appendFileSync(this.filePath, chunk, "utf-8");
|
|
217
214
|
}
|
|
218
215
|
|
|
219
|
-
/** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
|
|
216
|
+
/** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
|
|
220
217
|
end(attachments?: string[]): void {
|
|
221
218
|
fs.appendFileSync(this.filePath, "\n\n", "utf-8");
|
|
222
219
|
if (attachments?.length) {
|
|
223
220
|
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
// Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
|
|
222
|
+
const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
|
|
223
|
+
let lastMatch: RegExpExecArray | null = null;
|
|
224
|
+
let m;
|
|
225
|
+
while ((m = pattern.exec(raw)) !== null) lastMatch = m;
|
|
226
|
+
if (lastMatch) {
|
|
227
|
+
const before = raw.slice(0, lastMatch.index);
|
|
228
|
+
const after = raw.slice(lastMatch.index + lastMatch[0].length);
|
|
229
|
+
const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
|
|
230
|
+
fs.writeFileSync(this.filePath, updated, "utf-8");
|
|
231
|
+
}
|
|
226
232
|
}
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Splice a user message into a running assistant stream.
|
|
238
|
+
* Ends the current assistant block, writes the user message,
|
|
239
|
+
* then opens a new assistant block — all as direct file appends.
|
|
240
|
+
* The existing StreamingMessageWriter keeps working because its
|
|
241
|
+
* write() is just appendFileSync, so subsequent chunks land in
|
|
242
|
+
* the new assistant block.
|
|
243
|
+
*/
|
|
244
|
+
export function spliceUserMessage(
|
|
245
|
+
taskDir: string,
|
|
246
|
+
runId: string,
|
|
247
|
+
userMsg: ConversationMessage,
|
|
248
|
+
/** Optional text to append to the current assistant block before ending it. */
|
|
249
|
+
assistantAppend?: string,
|
|
250
|
+
): void {
|
|
251
|
+
const filePath = path.join(taskDir, runId, "TASKRUN.md");
|
|
252
|
+
// 1. Optionally append to the current assistant block (e.g. the input questions)
|
|
253
|
+
if (assistantAppend) {
|
|
254
|
+
fs.appendFileSync(filePath, assistantAppend, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
// 2. End the current assistant block
|
|
257
|
+
fs.appendFileSync(filePath, "\n\n", "utf-8");
|
|
258
|
+
// 3. Write the user message
|
|
259
|
+
appendRunMessage(taskDir, runId, userMsg);
|
|
260
|
+
// 4. Open a new assistant block for subsequent agent output
|
|
261
|
+
const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
|
|
262
|
+
fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
|
|
263
|
+
}
|
|
264
|
+
|
|
230
265
|
/**
|
|
231
266
|
* Read conversation messages from a run's TASKRUN.md file.
|
|
232
267
|
*/
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as os from "os";
|
|
3
3
|
import { StringCodec, type NatsConnection } from "nats";
|
|
4
|
-
import {
|
|
4
|
+
import { validateClient, addClient } from "../client-store.js";
|
|
5
5
|
import { registerPending } from "../pending-requests.js";
|
|
6
|
-
import
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
|
|
7
8
|
import type { HostConfig, RpcMessage, RequiredPermission } from "../types.js";
|
|
8
9
|
|
|
9
10
|
const PWA_ORIGIN = "https://app.palmier.me";
|
|
@@ -99,6 +100,18 @@ export function detectLanIp(): string {
|
|
|
99
100
|
return "127.0.0.1";
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
/** Find the latest (highest-numbered) run directory for a task. */
|
|
104
|
+
function findLatestRunId(taskDir: string): string | null {
|
|
105
|
+
try {
|
|
106
|
+
const dirs = fs.readdirSync(taskDir)
|
|
107
|
+
.filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
|
|
108
|
+
.sort();
|
|
109
|
+
return dirs.length > 0 ? dirs[dirs.length - 1] : null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
/**
|
|
103
116
|
* Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
|
|
104
117
|
* localhost-only agent endpoints (notify, request-input, confirmation, permission).
|
|
@@ -132,10 +145,10 @@ export async function startHttpTransport(
|
|
|
132
145
|
function checkAuth(req: http.IncomingMessage): boolean {
|
|
133
146
|
const auth = req.headers.authorization;
|
|
134
147
|
if (!auth || !auth.startsWith("Bearer ")) return false;
|
|
135
|
-
return
|
|
148
|
+
return validateClient(auth.slice(7));
|
|
136
149
|
}
|
|
137
150
|
|
|
138
|
-
function
|
|
151
|
+
function extractClientToken(req: http.IncomingMessage): string | undefined {
|
|
139
152
|
const auth = req.headers.authorization;
|
|
140
153
|
if (!auth || !auth.startsWith("Bearer ")) return undefined;
|
|
141
154
|
return auth.slice(7);
|
|
@@ -262,6 +275,9 @@ export async function startHttpTransport(
|
|
|
262
275
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
263
276
|
const task = parseTaskFile(taskDir);
|
|
264
277
|
|
|
278
|
+
// Resolve runId: use provided value, otherwise find the latest run directory
|
|
279
|
+
const effectiveRunId = runId ?? findLatestRunId(taskDir);
|
|
280
|
+
|
|
265
281
|
const pendingPromise = registerPending(taskId, "input", descriptions);
|
|
266
282
|
|
|
267
283
|
await publishEvent(taskId, {
|
|
@@ -273,17 +289,20 @@ export async function startHttpTransport(
|
|
|
273
289
|
|
|
274
290
|
const response = await pendingPromise;
|
|
275
291
|
|
|
292
|
+
const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
|
|
293
|
+
|
|
276
294
|
if (response.length === 1 && response[0] === "aborted") {
|
|
277
295
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
|
|
278
|
-
if (
|
|
279
|
-
|
|
296
|
+
if (effectiveRunId) {
|
|
297
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
|
|
298
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
280
299
|
}
|
|
281
300
|
sendJson(res, 200, { aborted: true });
|
|
282
301
|
} else {
|
|
283
302
|
await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
|
|
284
|
-
if (
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
if (effectiveRunId) {
|
|
304
|
+
spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
|
|
305
|
+
await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
|
|
287
306
|
}
|
|
288
307
|
sendJson(res, 200, { values: response });
|
|
289
308
|
}
|
|
@@ -375,11 +394,11 @@ export async function startHttpTransport(
|
|
|
375
394
|
const pending = pendingPairs.get(code);
|
|
376
395
|
if (!pending) { sendJson(res, 401, { error: "Invalid code" }); return; }
|
|
377
396
|
|
|
378
|
-
const
|
|
397
|
+
const client = addClient(label);
|
|
379
398
|
const ip = detectLanIp();
|
|
380
399
|
const response: Record<string, unknown> = {
|
|
381
400
|
hostId: config.hostId,
|
|
382
|
-
|
|
401
|
+
clientToken: client.token,
|
|
383
402
|
directUrl: `http://${ip}:${port}`,
|
|
384
403
|
};
|
|
385
404
|
|
|
@@ -457,11 +476,11 @@ export async function startHttpTransport(
|
|
|
457
476
|
}
|
|
458
477
|
} catch { sendJson(res, 400, { error: "Invalid JSON" }); return; }
|
|
459
478
|
|
|
460
|
-
const
|
|
479
|
+
const clientToken = extractClientToken(req);
|
|
461
480
|
console.log(`[http] RPC: ${method}`);
|
|
462
481
|
|
|
463
482
|
try {
|
|
464
|
-
const response = await handleRpc({ method, params,
|
|
483
|
+
const response = await handleRpc({ method, params, clientToken, localhost: isLocalhost(req) });
|
|
465
484
|
console.log(`[http] RPC done: ${method}`, JSON.stringify(response).slice(0, 200));
|
|
466
485
|
sendJson(res, 200, response);
|
|
467
486
|
} catch (err) {
|
|
@@ -50,15 +50,15 @@ export async function startNatsTransport(
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Extract
|
|
54
|
-
const
|
|
55
|
-
delete params.
|
|
53
|
+
// Extract clientToken from params (PWA includes it in the payload)
|
|
54
|
+
const clientToken = typeof params.clientToken === "string" ? params.clientToken : undefined;
|
|
55
|
+
delete params.clientToken;
|
|
56
56
|
|
|
57
57
|
console.log(`[nats] RPC: ${method}`);
|
|
58
58
|
|
|
59
59
|
let response: unknown;
|
|
60
60
|
try {
|
|
61
|
-
response = await handleRpc({ method, params,
|
|
61
|
+
response = await handleRpc({ method, params, clientToken });
|
|
62
62
|
} catch (err) {
|
|
63
63
|
console.error(`[nats] RPC error (${method}):`, err);
|
|
64
64
|
response = { error: String(err) };
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface HostConfig {
|
|
|
9
9
|
// Detected agent CLIs
|
|
10
10
|
agents?: Array<{ key: string; label: string }>;
|
|
11
11
|
|
|
12
|
-
// HTTP server port (default
|
|
12
|
+
// HTTP server port (default 9966)
|
|
13
13
|
httpPort?: number;
|
|
14
14
|
// Whether to accept non-localhost HTTP connections
|
|
15
15
|
lanEnabled?: boolean;
|
|
@@ -23,6 +23,7 @@ export interface TaskFrontmatter {
|
|
|
23
23
|
triggers: Trigger[];
|
|
24
24
|
triggers_enabled: boolean;
|
|
25
25
|
requires_confirmation: boolean;
|
|
26
|
+
yolo_mode?: boolean;
|
|
26
27
|
permissions?: RequiredPermission[];
|
|
27
28
|
command?: string;
|
|
28
29
|
}
|
|
@@ -78,7 +79,7 @@ export interface ConversationMessage {
|
|
|
78
79
|
export interface RpcMessage {
|
|
79
80
|
method: string;
|
|
80
81
|
params: Record<string, unknown>;
|
|
81
|
-
|
|
82
|
-
/** Trusted localhost request — skip
|
|
82
|
+
clientToken?: string;
|
|
83
|
+
/** Trusted localhost request — skip client validation. */
|
|
83
84
|
localhost?: boolean;
|
|
84
85
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const templatePath = path.join(__dirname, "..", "src", "agents", "agent-instructions.md");
|
|
9
|
+
const template = fs.readFileSync(templatePath, "utf-8");
|
|
10
|
+
|
|
11
|
+
/** Minimal replica of getAgentInstructions that doesn't need host.json */
|
|
12
|
+
function buildInstructions(taskId: string, skipPermissions?: boolean): string {
|
|
13
|
+
let instructions = template
|
|
14
|
+
.replace(/\{\{PORT\}\}/g, "9966")
|
|
15
|
+
.replace(/\{\{TASK_ID\}\}/g, taskId);
|
|
16
|
+
if (skipPermissions) {
|
|
17
|
+
instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
|
|
18
|
+
}
|
|
19
|
+
return instructions;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("getAgentInstructions", () => {
|
|
23
|
+
it("includes Permissions section by default", () => {
|
|
24
|
+
const result = buildInstructions("test-task-id");
|
|
25
|
+
assert.match(result, /## Permissions/);
|
|
26
|
+
assert.match(result, /PALMIER_PERMISSION/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("strips Permissions section when skipPermissions is true", () => {
|
|
30
|
+
const result = buildInstructions("test-task-id", true);
|
|
31
|
+
assert.doesNotMatch(result, /## Permissions/);
|
|
32
|
+
assert.doesNotMatch(result, /PALMIER_PERMISSION/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("preserves other sections when Permissions is stripped", () => {
|
|
36
|
+
const result = buildInstructions("test-task-id", true);
|
|
37
|
+
assert.match(result, /## Reporting Output/);
|
|
38
|
+
assert.match(result, /## Completion/);
|
|
39
|
+
assert.match(result, /## HTTP Endpoints/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("replaces template variables", () => {
|
|
43
|
+
const result = buildInstructions("my-task-123");
|
|
44
|
+
assert.match(result, /my-task-123/);
|
|
45
|
+
assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
|
|
46
|
+
assert.doesNotMatch(result, /\{\{PORT\}\}/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -38,6 +38,11 @@ describe("parseReportFiles", () => {
|
|
|
38
38
|
it("trims whitespace from file names", () => {
|
|
39
39
|
assert.deepEqual(parseReportFiles("[PALMIER_REPORT] report.md "), ["report.md"]);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
it("ignores placeholder examples from echoed prompt", () => {
|
|
43
|
+
const output = "[PALMIER_REPORT] <filename>\n[PALMIER_REPORT] actual-report.md";
|
|
44
|
+
assert.deepEqual(parseReportFiles(output), ["actual-report.md"]);
|
|
45
|
+
});
|
|
41
46
|
});
|
|
42
47
|
|
|
43
48
|
describe("parsePermissions", () => {
|
|
@@ -57,5 +62,12 @@ describe("parsePermissions", () => {
|
|
|
57
62
|
it("returns empty array when no permissions", () => {
|
|
58
63
|
assert.deepEqual(parsePermissions("no permissions"), []);
|
|
59
64
|
});
|
|
65
|
+
|
|
66
|
+
it("ignores placeholder examples from echoed prompt", () => {
|
|
67
|
+
const output = "[PALMIER_PERMISSION] <tool_name> | <description>\n[PALMIER_PERMISSION] Read | Read files";
|
|
68
|
+
const perms = parsePermissions(output);
|
|
69
|
+
assert.equal(perms.length, 1);
|
|
70
|
+
assert.deepEqual(perms[0], { name: "Read", description: "Read files" });
|
|
71
|
+
});
|
|
60
72
|
});
|
|
61
73
|
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
|
|
2
|
-
export async function sessionsListCommand() {
|
|
3
|
-
const sessions = loadSessions();
|
|
4
|
-
if (sessions.length === 0) {
|
|
5
|
-
console.log("No active sessions.");
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
console.log(`${sessions.length} active session(s):\n`);
|
|
9
|
-
for (const s of sessions) {
|
|
10
|
-
const label = s.label ? ` (${s.label})` : "";
|
|
11
|
-
console.log(` ${s.token}${label} created ${s.createdAt}`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export async function sessionsRevokeCommand(token) {
|
|
15
|
-
if (revokeSession(token)) {
|
|
16
|
-
console.log("Session revoked.");
|
|
17
|
-
}
|
|
18
|
-
else {
|
|
19
|
-
console.error("Session not found.");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
export async function sessionsRevokeAllCommand() {
|
|
24
|
-
const count = revokeAllSessions();
|
|
25
|
-
console.log(`Revoked ${count} session(s).`);
|
|
26
|
-
}
|
|
27
|
-
//# sourceMappingURL=sessions.js.map
|
package/dist/session-store.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export interface SessionEntry {
|
|
2
|
-
token: string;
|
|
3
|
-
createdAt: string;
|
|
4
|
-
label?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function loadSessions(): SessionEntry[];
|
|
7
|
-
export declare function addSession(label?: string): SessionEntry;
|
|
8
|
-
export declare function revokeSession(token: string): boolean;
|
|
9
|
-
export declare function revokeAllSessions(): number;
|
|
10
|
-
export declare function validateSession(token: string): boolean;
|
|
11
|
-
export declare function hasSessions(): boolean;
|
|
12
|
-
//# sourceMappingURL=session-store.d.ts.map
|
package/dist/session-store.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
|
-
import { CONFIG_DIR } from "./config.js";
|
|
5
|
-
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
6
|
-
function readFile() {
|
|
7
|
-
try {
|
|
8
|
-
if (!fs.existsSync(SESSIONS_FILE))
|
|
9
|
-
return [];
|
|
10
|
-
const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
|
|
11
|
-
return JSON.parse(raw);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function writeFile(sessions) {
|
|
18
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
19
|
-
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
|
|
20
|
-
}
|
|
21
|
-
export function loadSessions() {
|
|
22
|
-
return readFile();
|
|
23
|
-
}
|
|
24
|
-
export function addSession(label) {
|
|
25
|
-
const sessions = readFile();
|
|
26
|
-
const entry = {
|
|
27
|
-
token: randomBytes(32).toString("hex"),
|
|
28
|
-
createdAt: new Date().toISOString(),
|
|
29
|
-
...(label ? { label } : {}),
|
|
30
|
-
};
|
|
31
|
-
sessions.push(entry);
|
|
32
|
-
writeFile(sessions);
|
|
33
|
-
return entry;
|
|
34
|
-
}
|
|
35
|
-
export function revokeSession(token) {
|
|
36
|
-
const sessions = readFile();
|
|
37
|
-
const idx = sessions.findIndex((s) => s.token === token);
|
|
38
|
-
if (idx === -1)
|
|
39
|
-
return false;
|
|
40
|
-
sessions.splice(idx, 1);
|
|
41
|
-
writeFile(sessions);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
export function revokeAllSessions() {
|
|
45
|
-
const sessions = readFile();
|
|
46
|
-
const count = sessions.length;
|
|
47
|
-
writeFile([]);
|
|
48
|
-
return count;
|
|
49
|
-
}
|
|
50
|
-
export function validateSession(token) {
|
|
51
|
-
const sessions = readFile();
|
|
52
|
-
return sessions.some((s) => s.token === token);
|
|
53
|
-
}
|
|
54
|
-
export function hasSessions() {
|
|
55
|
-
return readFile().length > 0;
|
|
56
|
-
}
|
|
57
|
-
//# sourceMappingURL=session-store.js.map
|
package/src/commands/sessions.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { loadSessions, revokeSession, revokeAllSessions } from "../session-store.js";
|
|
2
|
-
|
|
3
|
-
export async function sessionsListCommand(): Promise<void> {
|
|
4
|
-
const sessions = loadSessions();
|
|
5
|
-
if (sessions.length === 0) {
|
|
6
|
-
console.log("No active sessions.");
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
console.log(`${sessions.length} active session(s):\n`);
|
|
11
|
-
for (const s of sessions) {
|
|
12
|
-
const label = s.label ? ` (${s.label})` : "";
|
|
13
|
-
console.log(` ${s.token}${label} created ${s.createdAt}`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function sessionsRevokeCommand(token: string): Promise<void> {
|
|
18
|
-
if (revokeSession(token)) {
|
|
19
|
-
console.log("Session revoked.");
|
|
20
|
-
} else {
|
|
21
|
-
console.error("Session not found.");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function sessionsRevokeAllCommand(): Promise<void> {
|
|
27
|
-
const count = revokeAllSessions();
|
|
28
|
-
console.log(`Revoked ${count} session(s).`);
|
|
29
|
-
}
|
package/src/session-store.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { randomBytes } from "crypto";
|
|
4
|
-
import { CONFIG_DIR } from "./config.js";
|
|
5
|
-
|
|
6
|
-
const SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
|
|
7
|
-
|
|
8
|
-
export interface SessionEntry {
|
|
9
|
-
token: string;
|
|
10
|
-
createdAt: string;
|
|
11
|
-
label?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function readFile(): SessionEntry[] {
|
|
15
|
-
try {
|
|
16
|
-
if (!fs.existsSync(SESSIONS_FILE)) return [];
|
|
17
|
-
const raw = fs.readFileSync(SESSIONS_FILE, "utf-8");
|
|
18
|
-
return JSON.parse(raw) as SessionEntry[];
|
|
19
|
-
} catch {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function writeFile(sessions: SessionEntry[]): void {
|
|
25
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
-
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2), "utf-8");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function loadSessions(): SessionEntry[] {
|
|
30
|
-
return readFile();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function addSession(label?: string): SessionEntry {
|
|
34
|
-
const sessions = readFile();
|
|
35
|
-
const entry: SessionEntry = {
|
|
36
|
-
token: randomBytes(32).toString("hex"),
|
|
37
|
-
createdAt: new Date().toISOString(),
|
|
38
|
-
...(label ? { label } : {}),
|
|
39
|
-
};
|
|
40
|
-
sessions.push(entry);
|
|
41
|
-
writeFile(sessions);
|
|
42
|
-
return entry;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function revokeSession(token: string): boolean {
|
|
46
|
-
const sessions = readFile();
|
|
47
|
-
const idx = sessions.findIndex((s) => s.token === token);
|
|
48
|
-
if (idx === -1) return false;
|
|
49
|
-
sessions.splice(idx, 1);
|
|
50
|
-
writeFile(sessions);
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function revokeAllSessions(): number {
|
|
55
|
-
const sessions = readFile();
|
|
56
|
-
const count = sessions.length;
|
|
57
|
-
writeFile([]);
|
|
58
|
-
return count;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateSession(token: string): boolean {
|
|
62
|
-
const sessions = readFile();
|
|
63
|
-
return sessions.some((s) => s.token === token);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function hasSessions(): boolean {
|
|
67
|
-
return readFile().length > 0;
|
|
68
|
-
}
|