@delega-dev/cli 1.0.4 → 1.0.6

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 CHANGED
@@ -79,31 +79,60 @@ delega stats # Show usage statistics
79
79
 
80
80
  ```bash
81
81
  --json # Output raw JSON for any command
82
- --api-url <url> # Override API URL (for self-hosted instances)
82
+ --api-url <url> # Override API URL
83
83
  --version # Show version
84
84
  --help # Show help
85
85
  ```
86
86
 
87
87
  ## Configuration
88
88
 
89
- Config is stored in `~/.delega/config.json`:
89
+ Non-secret CLI settings are stored in `~/.delega/config.json`:
90
90
 
91
91
  ```json
92
92
  {
93
- "api_key": "dlg_...",
94
93
  "api_url": "https://api.delega.dev"
95
94
  }
96
95
  ```
97
96
 
97
+ `delega login` stores API keys in the OS credential store when one is available:
98
+
99
+ - macOS: Keychain
100
+ - Linux: libsecret keyring via `secret-tool`
101
+ - Windows: DPAPI-protected user storage
102
+
103
+ Existing `api_key` entries in `~/.delega/config.json` are still read for backward compatibility until the next successful `delega login`.
104
+
98
105
  ## Environment Variables
99
106
 
100
107
  | Variable | Description |
101
108
  |---|---|
102
- | `DELEGA_API_KEY` | API key (overrides config file) |
109
+ | `DELEGA_API_KEY` | API key (overrides secure storage and config) |
103
110
  | `DELEGA_API_URL` | API base URL (overrides config file) |
104
111
 
105
112
  Environment variables take precedence over the config file.
106
113
 
114
+ ## Hosted vs Self-Hosted
115
+
116
+ The CLI defaults to the hosted API at `https://api.delega.dev/v1`.
117
+
118
+ For self-hosted deployments:
119
+
120
+ ```bash
121
+ export DELEGA_API_URL="http://localhost:18890"
122
+ # or for a remote reverse-proxied instance:
123
+ export DELEGA_API_URL="https://delega.yourcompany.com/api"
124
+ ```
125
+
126
+ Bare localhost URLs automatically use the self-hosted `/api` namespace. For remote self-hosted instances, include `/api` explicitly.
127
+
128
+ ## Security Notes
129
+
130
+ - `delega login` now hides API key input instead of echoing it back to the terminal.
131
+ - `delega login` stores API keys in the OS credential store instead of plaintext config when secure storage is available.
132
+ - `~/.delega/config.json` is written with owner-only permissions (`0600`), and the config directory is locked to `0700`.
133
+ - Remote API URLs must use `https://`; plain `http://` is only accepted for `localhost` / `127.0.0.1`.
134
+ - On servers that do not expose `/agent/me`, `delega login` and `delega whoami` fall back to generic authentication checks instead of printing hosted account metadata.
135
+
107
136
  ## License
108
137
 
109
138
  MIT
package/SECURITY.md ADDED
@@ -0,0 +1,20 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Do not open a public GitHub issue for security-sensitive reports.
6
+
7
+ Email `hello@delega.dev` with:
8
+
9
+ - A clear description of the issue
10
+ - Steps to reproduce it
11
+ - The affected version or commit, if known
12
+ - Any suggested mitigation or fix
13
+
14
+ Use a subject line like `Security report: delega-cli`.
15
+
16
+ We will acknowledge receipt and work on triage privately.
17
+
18
+ ## Supported Versions
19
+
20
+ Security fixes are applied to the latest published release and the current `main` branch.
package/dist/api.d.ts CHANGED
@@ -2,4 +2,10 @@ export interface ApiError {
2
2
  error?: string;
3
3
  message?: string;
4
4
  }
5
+ export interface ApiResponse<T = unknown> {
6
+ ok: boolean;
7
+ status: number;
8
+ data: T | ApiError;
9
+ }
10
+ export declare function apiRequest<T = unknown>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>>;
5
11
  export declare function apiCall<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
package/dist/api.js CHANGED
@@ -1,11 +1,20 @@
1
1
  import { getApiKey, getApiUrl } from "./config.js";
2
- export async function apiCall(method, path, body) {
2
+ export async function apiRequest(method, path, body) {
3
3
  const apiKey = getApiKey();
4
4
  if (!apiKey) {
5
5
  console.error("Not authenticated. Run: delega login");
6
6
  process.exit(1);
7
7
  }
8
- const url = getApiUrl() + path;
8
+ let apiBase;
9
+ try {
10
+ apiBase = getApiUrl();
11
+ }
12
+ catch (err) {
13
+ const msg = err instanceof Error ? err.message : String(err);
14
+ console.error(`Configuration error: ${msg}`);
15
+ process.exit(1);
16
+ }
17
+ const url = apiBase + path;
9
18
  const headers = {
10
19
  "X-Agent-Key": apiKey,
11
20
  "Content-Type": "application/json",
@@ -23,10 +32,6 @@ export async function apiCall(method, path, body) {
23
32
  console.error(`Connection error: ${msg}`);
24
33
  process.exit(1);
25
34
  }
26
- if (res.status === 401) {
27
- console.error("Authentication failed. Run: delega login");
28
- process.exit(1);
29
- }
30
35
  let data;
31
36
  const text = await res.text();
32
37
  try {
@@ -35,11 +40,23 @@ export async function apiCall(method, path, body) {
35
40
  catch {
36
41
  data = { message: text };
37
42
  }
38
- if (!res.ok) {
39
- const errData = data;
40
- const msg = errData.error || errData.message || `Request failed (${res.status})`;
43
+ return {
44
+ ok: res.ok,
45
+ status: res.status,
46
+ data: data,
47
+ };
48
+ }
49
+ export async function apiCall(method, path, body) {
50
+ const result = await apiRequest(method, path, body);
51
+ if (result.status === 401) {
52
+ console.error("Authentication failed. Run: delega login");
53
+ process.exit(1);
54
+ }
55
+ if (!result.ok) {
56
+ const errData = result.data;
57
+ const msg = errData.error || errData.message || `Request failed (${result.status})`;
41
58
  console.error(`Error: ${msg}`);
42
59
  process.exit(1);
43
60
  }
44
- return data;
61
+ return result.data;
45
62
  }
@@ -19,7 +19,7 @@ const agentsList = new Command("list")
19
19
  .description("List agents")
20
20
  .option("--json", "Output raw JSON")
21
21
  .action(async (opts) => {
22
- const data = await apiCall("GET", "/v1/agents");
22
+ const data = await apiCall("GET", "/agents");
23
23
  if (opts.json) {
24
24
  console.log(JSON.stringify(data, null, 2));
25
25
  return;
@@ -47,7 +47,7 @@ const agentsCreate = new Command("create")
47
47
  const body = { name };
48
48
  if (opts.displayName)
49
49
  body.display_name = opts.displayName;
50
- const agent = await apiCall("POST", "/v1/agents", body);
50
+ const agent = await apiCall("POST", "/agents", body);
51
51
  if (opts.json) {
52
52
  console.log(JSON.stringify(agent, null, 2));
53
53
  return;
@@ -73,7 +73,7 @@ const agentsRotate = new Command("rotate")
73
73
  console.log("Cancelled.");
74
74
  return;
75
75
  }
76
- const result = await apiCall("POST", `/v1/agents/${id}/rotate-key`);
76
+ const result = await apiCall("POST", `/agents/${id}/rotate-key`);
77
77
  console.log();
78
78
  console.log(` New API Key: ${chalk.cyan.bold(result.api_key)}`);
79
79
  console.log(chalk.yellow(" Save this key — it will not be shown again."));
@@ -1,15 +1,26 @@
1
1
  import { Command } from "commander";
2
2
  import node_readline from "node:readline";
3
- import { saveConfig, loadConfig } from "../config.js";
3
+ import { saveConfig, loadConfig, normalizeApiUrl, persistApiKey } from "../config.js";
4
4
  import { printBanner } from "../ui.js";
5
- async function prompt(question) {
5
+ async function promptSecret(question) {
6
+ const mutedOutput = {
7
+ muted: false,
8
+ write(chunk) {
9
+ if (!this.muted || chunk.includes(question)) {
10
+ process.stdout.write(chunk);
11
+ }
12
+ },
13
+ };
6
14
  const rl = node_readline.createInterface({
7
15
  input: process.stdin,
8
- output: process.stdout,
16
+ output: mutedOutput,
17
+ terminal: true,
9
18
  });
19
+ mutedOutput.muted = true;
10
20
  return new Promise((resolve) => {
11
21
  rl.question(question, (answer) => {
12
22
  rl.close();
23
+ process.stdout.write("\n");
13
24
  resolve(answer.trim());
14
25
  });
15
26
  });
@@ -18,7 +29,7 @@ export const loginCommand = new Command("login")
18
29
  .description("Authenticate with the Delega API")
19
30
  .action(async () => {
20
31
  printBanner();
21
- const key = await prompt("Enter your API key (starts with dlg_): ");
32
+ const key = await promptSecret("Enter your API key (starts with dlg_): ");
22
33
  if (!key) {
23
34
  console.error("No key provided.");
24
35
  process.exit(1);
@@ -29,10 +40,18 @@ export const loginCommand = new Command("login")
29
40
  }
30
41
  // Validate by calling the API
31
42
  const config = loadConfig();
32
- const apiUrl = config.api_url || process.env.DELEGA_API_URL || "https://api.delega.dev";
43
+ let apiUrl;
44
+ try {
45
+ apiUrl = normalizeApiUrl(config.api_url || process.env.DELEGA_API_URL || "https://api.delega.dev");
46
+ }
47
+ catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ console.error(`Configuration error: ${msg}`);
50
+ process.exit(1);
51
+ }
33
52
  let res;
34
53
  try {
35
- res = await fetch(`${apiUrl}/v1/agents`, {
54
+ res = await fetch(`${apiUrl}/agent/me`, {
36
55
  headers: {
37
56
  "X-Agent-Key": key,
38
57
  "Content-Type": "application/json",
@@ -44,23 +63,59 @@ export const loginCommand = new Command("login")
44
63
  console.error(`Connection error: ${msg}`);
45
64
  process.exit(1);
46
65
  }
47
- if (!res.ok) {
66
+ if (!res.ok && res.status !== 404) {
48
67
  console.error("Invalid API key. Authentication failed.");
49
68
  process.exit(1);
50
69
  }
51
70
  let agentName = "agent";
52
- try {
53
- const data = (await res.json());
54
- if (Array.isArray(data) && data.length > 0) {
55
- agentName = data[0].display_name || data[0].name;
71
+ let validatedWithoutMetadata = false;
72
+ if (res.ok) {
73
+ try {
74
+ const data = (await res.json());
75
+ if (data.agent?.name) {
76
+ agentName = data.agent.display_name || data.agent.name;
77
+ }
56
78
  }
57
- else if (!Array.isArray(data) && data.name) {
58
- agentName = data.display_name || data.name;
79
+ catch {
80
+ // Proceed with default name
81
+ }
82
+ }
83
+ else {
84
+ try {
85
+ res = await fetch(`${apiUrl}/tasks?completed=true`, {
86
+ headers: {
87
+ "X-Agent-Key": key,
88
+ "Content-Type": "application/json",
89
+ },
90
+ });
59
91
  }
92
+ catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ console.error(`Connection error: ${msg}`);
95
+ process.exit(1);
96
+ }
97
+ if (!res.ok) {
98
+ console.error("Invalid API key. Authentication failed.");
99
+ process.exit(1);
100
+ }
101
+ validatedWithoutMetadata = true;
102
+ }
103
+ let storageLocation;
104
+ try {
105
+ storageLocation = persistApiKey(key);
106
+ }
107
+ catch (err) {
108
+ const msg = err instanceof Error ? err.message : String(err);
109
+ console.error(`Unable to store API key securely: ${msg}`);
110
+ process.exit(1);
60
111
  }
61
- catch {
62
- // Proceed with default name
112
+ const nextConfig = { ...config };
113
+ delete nextConfig.api_key;
114
+ saveConfig(nextConfig);
115
+ if (validatedWithoutMetadata) {
116
+ console.log(`\nLogged in. Key saved to ${storageLocation}`);
117
+ console.log("Current server validated the key but does not expose /agent/me metadata.");
118
+ return;
63
119
  }
64
- saveConfig({ ...config, api_key: key });
65
- console.log(`\nLogged in as ${agentName}. Key saved to ~/.delega/config.json`);
120
+ console.log(`\nLogged in as ${agentName}. Key saved to ${storageLocation}`);
66
121
  });
@@ -5,7 +5,7 @@ export const statsCommand = new Command("stats")
5
5
  .description("Show usage statistics")
6
6
  .option("--json", "Output raw JSON")
7
7
  .action(async (opts) => {
8
- const data = await apiCall("GET", "/v1/stats");
8
+ const data = await apiCall("GET", "/stats");
9
9
  if (opts.json) {
10
10
  console.log(JSON.stringify(data, null, 2));
11
11
  return;
@@ -20,7 +20,7 @@ const tasksList = new Command("list")
20
20
  .option("--limit <n>", "Limit results", parseInt)
21
21
  .option("--json", "Output raw JSON")
22
22
  .action(async (opts) => {
23
- let path = "/v1/tasks";
23
+ let path = "/tasks";
24
24
  const params = [];
25
25
  if (opts.completed)
26
26
  params.push("completed=true");
@@ -62,7 +62,7 @@ const tasksCreate = new Command("create")
62
62
  body.labels = opts.labels.split(",").map((l) => l.trim());
63
63
  if (opts.due)
64
64
  body.due_date = opts.due;
65
- const task = await apiCall("POST", "/v1/tasks", body);
65
+ const task = await apiCall("POST", "/tasks", body);
66
66
  if (opts.json) {
67
67
  console.log(JSON.stringify(task, null, 2));
68
68
  return;
@@ -80,7 +80,7 @@ const tasksShow = new Command("show")
80
80
  .argument("<id>", "Task ID")
81
81
  .option("--json", "Output raw JSON")
82
82
  .action(async (id, opts) => {
83
- const task = await apiCall("GET", `/v1/tasks/${id}`);
83
+ const task = await apiCall("GET", `/tasks/${id}`);
84
84
  if (opts.json) {
85
85
  console.log(JSON.stringify(task, null, 2));
86
86
  return;
@@ -116,7 +116,7 @@ const tasksComplete = new Command("complete")
116
116
  .description("Mark a task as completed")
117
117
  .argument("<id>", "Task ID")
118
118
  .action(async (id) => {
119
- await apiCall("POST", `/v1/tasks/${id}/complete`);
119
+ await apiCall("POST", `/tasks/${id}/complete`);
120
120
  console.log(`Task ${id} completed.`);
121
121
  });
122
122
  const tasksDelete = new Command("delete")
@@ -128,7 +128,7 @@ const tasksDelete = new Command("delete")
128
128
  console.log("Cancelled.");
129
129
  return;
130
130
  }
131
- await apiCall("DELETE", `/v1/tasks/${id}`);
131
+ await apiCall("DELETE", `/tasks/${id}`);
132
132
  console.log(`Task ${id} deleted.`);
133
133
  });
134
134
  const tasksDelegate = new Command("delegate")
@@ -140,7 +140,7 @@ const tasksDelegate = new Command("delegate")
140
140
  const body = { assigned_to_agent_id: agentId };
141
141
  if (opts.content)
142
142
  body.content = opts.content;
143
- await apiCall("POST", `/v1/tasks/${taskId}/delegate`, body);
143
+ await apiCall("POST", `/tasks/${taskId}/delegate`, body);
144
144
  console.log(`Task delegated to ${agentId}.`);
145
145
  });
146
146
  export const tasksCommand = new Command("tasks")
@@ -1,32 +1,39 @@
1
1
  import { Command } from "commander";
2
- import { apiCall } from "../api.js";
2
+ import { apiCall, apiRequest } from "../api.js";
3
3
  import { label } from "../ui.js";
4
4
  export const whoamiCommand = new Command("whoami")
5
5
  .description("Show current authenticated agent")
6
6
  .action(async () => {
7
- const data = await apiCall("GET", "/v1/agents");
8
- let agent;
9
- if (Array.isArray(data)) {
10
- if (data.length === 0) {
11
- console.error("No agent found.");
7
+ const me = await apiRequest("GET", "/agent/me");
8
+ if (me.ok) {
9
+ const payload = me.data;
10
+ const agent = payload.agent;
11
+ if (!agent) {
12
+ console.error("Current server did not return agent details.");
12
13
  process.exit(1);
13
14
  }
14
- agent = data[0];
15
+ console.log();
16
+ label("Agent", agent.name);
17
+ if (agent.display_name) {
18
+ label("Display Name", agent.display_name);
19
+ }
20
+ if (payload.account?.email || agent.user?.email || agent.email) {
21
+ label("Email", payload.account?.email || agent.user?.email || agent.email || "");
22
+ }
23
+ if (payload.account?.plan || agent.user?.plan || agent.plan) {
24
+ label("Plan", payload.account?.plan || agent.user?.plan || agent.plan || "");
25
+ }
26
+ label("Active", agent.active !== false ? "yes" : "no");
27
+ console.log();
28
+ return;
15
29
  }
16
- else {
17
- agent = data;
30
+ if (me.status !== 404) {
31
+ await apiCall("GET", "/agent/me");
32
+ return;
18
33
  }
34
+ await apiCall("GET", "/tasks?completed=true");
19
35
  console.log();
20
- label("Agent", agent.name);
21
- if (agent.display_name) {
22
- label("Display Name", agent.display_name);
23
- }
24
- if (agent.user?.email || agent.email) {
25
- label("Email", agent.user?.email || agent.email || "");
26
- }
27
- if (agent.user?.plan || agent.plan) {
28
- label("Plan", agent.user?.plan || agent.plan || "");
29
- }
30
- label("Active", agent.active !== false ? "yes" : "no");
36
+ label("Authenticated", "yes");
37
+ label("Server", "Current API does not expose /agent/me");
31
38
  console.log();
32
39
  });
package/dist/config.d.ts CHANGED
@@ -4,5 +4,7 @@ export interface DelegaConfig {
4
4
  }
5
5
  export declare function loadConfig(): DelegaConfig;
6
6
  export declare function saveConfig(config: DelegaConfig): void;
7
+ export declare function persistApiKey(apiKey: string): string;
8
+ export declare function normalizeApiUrl(rawUrl: string): string;
7
9
  export declare function getApiKey(): string | undefined;
8
10
  export declare function getApiUrl(): string;
package/dist/config.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import node_fs from "node:fs";
2
2
  import node_path from "node:path";
3
3
  import node_os from "node:os";
4
+ import { loadStoredApiKey, storeApiKey } from "./secret-store.js";
5
+ const LOCAL_API_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
6
+ function normalizeHost(hostname) {
7
+ return hostname.replace(/^\[/, "").replace(/\]$/, "").toLowerCase();
8
+ }
9
+ function isLocalApiHost(hostname) {
10
+ return LOCAL_API_HOSTS.has(normalizeHost(hostname));
11
+ }
12
+ function defaultApiBasePath(hostname) {
13
+ return isLocalApiHost(hostname) ? "/api" : "/v1";
14
+ }
4
15
  function getConfigDir() {
5
16
  return node_path.join(node_os.homedir(), ".delega");
6
17
  }
@@ -23,15 +34,43 @@ export function loadConfig() {
23
34
  export function saveConfig(config) {
24
35
  const configDir = getConfigDir();
25
36
  if (!node_fs.existsSync(configDir)) {
26
- node_fs.mkdirSync(configDir, { recursive: true });
37
+ node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
38
+ }
39
+ node_fs.chmodSync(configDir, 0o700);
40
+ node_fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
41
+ node_fs.chmodSync(getConfigPath(), 0o600);
42
+ }
43
+ export function persistApiKey(apiKey) {
44
+ const storeLabel = storeApiKey(apiKey);
45
+ if (storeLabel) {
46
+ return storeLabel;
47
+ }
48
+ throw new Error("Secure credential storage is unavailable on this system. Set DELEGA_API_KEY manually instead.");
49
+ }
50
+ export function normalizeApiUrl(rawUrl) {
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(rawUrl);
54
+ }
55
+ catch {
56
+ throw new Error("Invalid Delega API URL");
57
+ }
58
+ if (parsed.protocol !== "https:" && !isLocalApiHost(parsed.hostname)) {
59
+ throw new Error("Delega API URL must use HTTPS unless it points to localhost");
27
60
  }
28
- node_fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
61
+ parsed.search = "";
62
+ parsed.hash = "";
63
+ const normalizedPath = parsed.pathname.replace(/\/+$/, "");
64
+ parsed.pathname = normalizedPath && normalizedPath !== "/"
65
+ ? normalizedPath
66
+ : defaultApiBasePath(parsed.hostname);
67
+ return parsed.toString().replace(/\/+$/, "");
29
68
  }
30
69
  export function getApiKey() {
31
- return process.env.DELEGA_API_KEY || loadConfig().api_key;
70
+ return process.env.DELEGA_API_KEY || loadStoredApiKey() || loadConfig().api_key;
32
71
  }
33
72
  export function getApiUrl() {
34
- return (process.env.DELEGA_API_URL ||
73
+ return normalizeApiUrl(process.env.DELEGA_API_URL ||
35
74
  loadConfig().api_url ||
36
75
  "https://api.delega.dev");
37
76
  }
@@ -0,0 +1,3 @@
1
+ export declare function secureStoreLabel(): string | undefined;
2
+ export declare function loadStoredApiKey(): string | undefined;
3
+ export declare function storeApiKey(apiKey: string): string | undefined;
@@ -0,0 +1,128 @@
1
+ import node_child_process from "node:child_process";
2
+ import node_fs from "node:fs";
3
+ import node_os from "node:os";
4
+ import node_path from "node:path";
5
+ const SERVICE_NAME = "@delega-dev/cli";
6
+ const ACCOUNT_NAME = "default";
7
+ const WINDOWS_SECRET_PATH = node_path.join(node_os.homedir(), ".delega", "api-key.dpapi");
8
+ function ensureConfigDir() {
9
+ const configDir = node_path.dirname(WINDOWS_SECRET_PATH);
10
+ if (!node_fs.existsSync(configDir)) {
11
+ node_fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
12
+ }
13
+ }
14
+ function isLinuxSecretToolAvailable() {
15
+ if (process.platform !== "linux") {
16
+ return false;
17
+ }
18
+ try {
19
+ node_child_process.execFileSync("sh", ["-lc", "command -v secret-tool >/dev/null 2>&1"], {
20
+ stdio: "ignore",
21
+ });
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ function readMacosKeychain() {
29
+ try {
30
+ return node_child_process.execFileSync("security", ["find-generic-password", "-a", ACCOUNT_NAME, "-s", SERVICE_NAME, "-w"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ function writeMacosKeychain(apiKey) {
37
+ node_child_process.execFileSync("security", ["add-generic-password", "-U", "-a", ACCOUNT_NAME, "-s", SERVICE_NAME, "-w", apiKey], { stdio: "ignore" });
38
+ }
39
+ function readLinuxSecretTool() {
40
+ if (!isLinuxSecretToolAvailable()) {
41
+ return undefined;
42
+ }
43
+ try {
44
+ return node_child_process.execFileSync("secret-tool", ["lookup", "service", SERVICE_NAME, "account", ACCOUNT_NAME], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
50
+ function writeLinuxSecretTool(apiKey) {
51
+ node_child_process.execFileSync("secret-tool", ["store", "--label", "Delega CLI API key", "service", SERVICE_NAME, "account", ACCOUNT_NAME], { input: apiKey, encoding: "utf-8", stdio: ["pipe", "ignore", "ignore"] });
52
+ }
53
+ function readWindowsProtectedFile() {
54
+ if (!node_fs.existsSync(WINDOWS_SECRET_PATH)) {
55
+ return undefined;
56
+ }
57
+ try {
58
+ return node_child_process.execFileSync("powershell.exe", [
59
+ "-NoProfile",
60
+ "-NonInteractive",
61
+ "-Command",
62
+ "$secure = Get-Content -Raw $env:DELEGA_SECRET_PATH | ConvertTo-SecureString; $cred = [System.Management.Automation.PSCredential]::new('delega', $secure); $cred.GetNetworkCredential().Password",
63
+ ], {
64
+ encoding: "utf-8",
65
+ stdio: ["ignore", "pipe", "ignore"],
66
+ env: { ...process.env, DELEGA_SECRET_PATH: WINDOWS_SECRET_PATH },
67
+ }).trim();
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
73
+ function writeWindowsProtectedFile(apiKey) {
74
+ ensureConfigDir();
75
+ const encrypted = node_child_process.execFileSync("powershell.exe", [
76
+ "-NoProfile",
77
+ "-NonInteractive",
78
+ "-Command",
79
+ "$secure = ConvertTo-SecureString $env:DELEGA_API_KEY -AsPlainText -Force; $secure | ConvertFrom-SecureString",
80
+ ], {
81
+ encoding: "utf-8",
82
+ stdio: ["ignore", "pipe", "ignore"],
83
+ env: { ...process.env, DELEGA_API_KEY: apiKey },
84
+ }).trim();
85
+ node_fs.writeFileSync(WINDOWS_SECRET_PATH, encrypted + "\n", {
86
+ encoding: "utf-8",
87
+ mode: 0o600,
88
+ });
89
+ }
90
+ export function secureStoreLabel() {
91
+ if (process.platform === "darwin") {
92
+ return "macOS Keychain";
93
+ }
94
+ if (process.platform === "linux" && isLinuxSecretToolAvailable()) {
95
+ return "libsecret keyring";
96
+ }
97
+ if (process.platform === "win32") {
98
+ return "Windows user-protected storage";
99
+ }
100
+ return undefined;
101
+ }
102
+ export function loadStoredApiKey() {
103
+ if (process.platform === "darwin") {
104
+ return readMacosKeychain();
105
+ }
106
+ if (process.platform === "linux") {
107
+ return readLinuxSecretTool();
108
+ }
109
+ if (process.platform === "win32") {
110
+ return readWindowsProtectedFile();
111
+ }
112
+ return undefined;
113
+ }
114
+ export function storeApiKey(apiKey) {
115
+ if (process.platform === "darwin") {
116
+ writeMacosKeychain(apiKey);
117
+ return "macOS Keychain";
118
+ }
119
+ if (process.platform === "linux" && isLinuxSecretToolAvailable()) {
120
+ writeLinuxSecretTool(apiKey);
121
+ return "libsecret keyring";
122
+ }
123
+ if (process.platform === "win32") {
124
+ writeWindowsProtectedFile(apiKey);
125
+ return "Windows user-protected storage";
126
+ }
127
+ return undefined;
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delega-dev/cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "CLI for Delega task API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,12 @@
24
24
  "type": "git",
25
25
  "url": "https://github.com/delega-dev/delega-cli"
26
26
  },
27
+ "files": [
28
+ "dist",
29
+ "LICENSE",
30
+ "README.md",
31
+ "SECURITY.md"
32
+ ],
27
33
  "keywords": [
28
34
  "delega",
29
35
  "task-management",