secretctl-cli 0.1.0__py3-none-any.whl
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.
- secretctl/__init__.py +3 -0
- secretctl/backend.py +176 -0
- secretctl/cli.py +230 -0
- secretctl_cli-0.1.0.dist-info/METADATA +130 -0
- secretctl_cli-0.1.0.dist-info/RECORD +8 -0
- secretctl_cli-0.1.0.dist-info/WHEEL +4 -0
- secretctl_cli-0.1.0.dist-info/entry_points.txt +2 -0
- secretctl_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
secretctl/__init__.py
ADDED
secretctl/backend.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Backend implementations for secret storage."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecretBackend(ABC):
|
|
9
|
+
"""Abstract base class for secret storage backends."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, account: str):
|
|
12
|
+
self.account = account
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def get(self, name: str) -> str | None:
|
|
16
|
+
"""Get a secret value."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def set(self, name: str, value: str) -> None:
|
|
21
|
+
"""Set a secret value."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def delete(self, name: str) -> None:
|
|
26
|
+
"""Delete a secret."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def list(self) -> list[str]:
|
|
31
|
+
"""List all secret names."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MacOSKeychain(SecretBackend):
|
|
36
|
+
"""macOS Keychain backend using security command."""
|
|
37
|
+
|
|
38
|
+
def get(self, name: str) -> str | None:
|
|
39
|
+
"""Get a secret from macOS Keychain."""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
[
|
|
43
|
+
"security",
|
|
44
|
+
"find-generic-password",
|
|
45
|
+
"-a", self.account,
|
|
46
|
+
"-s", name,
|
|
47
|
+
"-w",
|
|
48
|
+
],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
if result.returncode == 0:
|
|
53
|
+
return result.stdout.strip()
|
|
54
|
+
return None
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def set(self, name: str, value: str) -> None:
|
|
59
|
+
"""Set a secret in macOS Keychain."""
|
|
60
|
+
# Delete existing first (security doesn't update in place)
|
|
61
|
+
subprocess.run(
|
|
62
|
+
[
|
|
63
|
+
"security",
|
|
64
|
+
"delete-generic-password",
|
|
65
|
+
"-a", self.account,
|
|
66
|
+
"-s", name,
|
|
67
|
+
],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
)
|
|
70
|
+
# Add new
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
[
|
|
73
|
+
"security",
|
|
74
|
+
"add-generic-password",
|
|
75
|
+
"-a", self.account,
|
|
76
|
+
"-s", name,
|
|
77
|
+
"-w", value,
|
|
78
|
+
"-U", # Update if exists
|
|
79
|
+
],
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
)
|
|
83
|
+
if result.returncode != 0:
|
|
84
|
+
raise RuntimeError(f"Failed to store secret: {result.stderr}")
|
|
85
|
+
|
|
86
|
+
def delete(self, name: str) -> None:
|
|
87
|
+
"""Delete a secret from macOS Keychain."""
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
[
|
|
90
|
+
"security",
|
|
91
|
+
"delete-generic-password",
|
|
92
|
+
"-a", self.account,
|
|
93
|
+
"-s", name,
|
|
94
|
+
],
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
)
|
|
98
|
+
if result.returncode != 0 and "could not be found" not in result.stderr:
|
|
99
|
+
raise RuntimeError(f"Failed to delete secret: {result.stderr}")
|
|
100
|
+
|
|
101
|
+
def list(self) -> list[str]:
|
|
102
|
+
"""List all secrets for this account in macOS Keychain."""
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
["security", "dump-keychain"],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
)
|
|
108
|
+
if result.returncode != 0:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
secrets = []
|
|
112
|
+
current_account = None
|
|
113
|
+
current_service = None
|
|
114
|
+
|
|
115
|
+
for line in result.stdout.splitlines():
|
|
116
|
+
line = line.strip()
|
|
117
|
+
if '"acct"' in line and f'"{self.account}"' in line:
|
|
118
|
+
current_account = self.account
|
|
119
|
+
elif '"svce"' in line and current_account:
|
|
120
|
+
# Extract service name
|
|
121
|
+
# Format: "svce"<blob>="service_name"
|
|
122
|
+
if '="' in line:
|
|
123
|
+
current_service = line.split('="')[1].rstrip('"')
|
|
124
|
+
secrets.append(current_service)
|
|
125
|
+
current_account = None
|
|
126
|
+
current_service = None
|
|
127
|
+
|
|
128
|
+
return list(set(secrets)) # Deduplicate
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class LinuxKeyring(SecretBackend):
|
|
132
|
+
"""Linux keyring backend using the keyring library."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, account: str):
|
|
135
|
+
super().__init__(account)
|
|
136
|
+
try:
|
|
137
|
+
import keyring
|
|
138
|
+
self.keyring = keyring
|
|
139
|
+
except ImportError:
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
"keyring library required on Linux. "
|
|
142
|
+
"Install with: pip install secretctl[linux]"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def get(self, name: str) -> str | None:
|
|
146
|
+
"""Get a secret from Linux keyring."""
|
|
147
|
+
return self.keyring.get_password(self.account, name)
|
|
148
|
+
|
|
149
|
+
def set(self, name: str, value: str) -> None:
|
|
150
|
+
"""Set a secret in Linux keyring."""
|
|
151
|
+
self.keyring.set_password(self.account, name, value)
|
|
152
|
+
|
|
153
|
+
def delete(self, name: str) -> None:
|
|
154
|
+
"""Delete a secret from Linux keyring."""
|
|
155
|
+
try:
|
|
156
|
+
self.keyring.delete_password(self.account, name)
|
|
157
|
+
except self.keyring.errors.PasswordDeleteError:
|
|
158
|
+
pass # Already deleted or doesn't exist
|
|
159
|
+
|
|
160
|
+
def list(self) -> list[str]:
|
|
161
|
+
"""List all secrets (not easily supported on Linux keyring)."""
|
|
162
|
+
# Linux keyring doesn't have a native list function
|
|
163
|
+
# This is a limitation - users need to track their own keys
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_backend(account: str) -> SecretBackend:
|
|
168
|
+
"""Get the appropriate backend for the current platform."""
|
|
169
|
+
system = platform.system()
|
|
170
|
+
|
|
171
|
+
if system == "Darwin":
|
|
172
|
+
return MacOSKeychain(account)
|
|
173
|
+
elif system == "Linux":
|
|
174
|
+
return LinuxKeyring(account)
|
|
175
|
+
else:
|
|
176
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
secretctl/cli.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Main CLI entry point for secretctl."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .backend import get_backend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Context:
|
|
12
|
+
"""Shared context for all commands."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, json_output: bool = False, account: str = "secretctl"):
|
|
15
|
+
self.json_output = json_output
|
|
16
|
+
self.account = account
|
|
17
|
+
self.backend = get_backend(account)
|
|
18
|
+
|
|
19
|
+
def output(self, data: dict | list | str, success: bool = True):
|
|
20
|
+
"""Output data in the appropriate format."""
|
|
21
|
+
if self.json_output:
|
|
22
|
+
if isinstance(data, str):
|
|
23
|
+
data = {"message": data, "success": success}
|
|
24
|
+
elif isinstance(data, dict) and "success" not in data:
|
|
25
|
+
data = {**data, "success": success}
|
|
26
|
+
click.echo(json.dumps(data, indent=2))
|
|
27
|
+
else:
|
|
28
|
+
if isinstance(data, dict):
|
|
29
|
+
for key, value in data.items():
|
|
30
|
+
if key != "success":
|
|
31
|
+
click.echo(f"{key}: {value}")
|
|
32
|
+
elif isinstance(data, list):
|
|
33
|
+
for item in data:
|
|
34
|
+
click.echo(item)
|
|
35
|
+
else:
|
|
36
|
+
click.echo(data)
|
|
37
|
+
|
|
38
|
+
def error(self, message: str, exit_code: int = 1):
|
|
39
|
+
"""Output an error message and exit."""
|
|
40
|
+
if self.json_output:
|
|
41
|
+
click.echo(json.dumps({"error": message, "success": False}))
|
|
42
|
+
else:
|
|
43
|
+
click.echo(f"Error: {message}", err=True)
|
|
44
|
+
sys.exit(exit_code)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
pass_context = click.make_pass_decorator(Context, ensure=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@click.group()
|
|
51
|
+
@click.option("--json", "json_output", is_flag=True, help="Output in JSON format")
|
|
52
|
+
@click.option("--account", "-a", default="secretctl", help="Account/namespace for secrets")
|
|
53
|
+
@click.version_option(version=__version__, prog_name="secretctl")
|
|
54
|
+
@click.pass_context
|
|
55
|
+
def main(ctx, json_output: bool, account: str):
|
|
56
|
+
"""Cross-platform secret storage CLI for AI agents.
|
|
57
|
+
|
|
58
|
+
Store and retrieve secrets from the OS keychain without cloud sync
|
|
59
|
+
or biometrics. Designed for automation and AI agent workflows.
|
|
60
|
+
|
|
61
|
+
Examples:
|
|
62
|
+
secretctl set API_KEY sk-xxx
|
|
63
|
+
secretctl get API_KEY
|
|
64
|
+
secretctl list
|
|
65
|
+
"""
|
|
66
|
+
ctx.obj = Context(json_output=json_output, account=account)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@main.command()
|
|
70
|
+
@click.argument("name")
|
|
71
|
+
@click.argument("value")
|
|
72
|
+
@pass_context
|
|
73
|
+
def set(ctx, name: str, value: str):
|
|
74
|
+
"""Store a secret.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
secretctl set API_KEY sk-xxx123
|
|
78
|
+
secretctl set DATABASE_URL "postgres://user:pass@host/db"
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
ctx.backend.set(name, value)
|
|
82
|
+
ctx.output({"name": name, "stored": True, "account": ctx.account})
|
|
83
|
+
except Exception as e:
|
|
84
|
+
ctx.error(f"Failed to store secret: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@main.command()
|
|
88
|
+
@click.argument("name")
|
|
89
|
+
@pass_context
|
|
90
|
+
def get(ctx, name: str):
|
|
91
|
+
"""Retrieve a secret.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
secretctl get API_KEY
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
value = ctx.backend.get(name)
|
|
98
|
+
if value is None:
|
|
99
|
+
ctx.error(f"Secret not found: {name}")
|
|
100
|
+
ctx.output({"name": name, "value": value})
|
|
101
|
+
except Exception as e:
|
|
102
|
+
ctx.error(f"Failed to retrieve secret: {e}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@main.command()
|
|
106
|
+
@click.argument("name")
|
|
107
|
+
@pass_context
|
|
108
|
+
def delete(ctx, name: str):
|
|
109
|
+
"""Delete a secret.
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
secretctl delete API_KEY
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
ctx.backend.delete(name)
|
|
116
|
+
ctx.output({"name": name, "deleted": True})
|
|
117
|
+
except Exception as e:
|
|
118
|
+
ctx.error(f"Failed to delete secret: {e}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@main.command("list")
|
|
122
|
+
@pass_context
|
|
123
|
+
def list_secrets(ctx):
|
|
124
|
+
"""List all secret names (values hidden).
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
secretctl list
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
secrets = ctx.backend.list()
|
|
131
|
+
ctx.output({"secrets": secrets, "count": len(secrets), "account": ctx.account})
|
|
132
|
+
except Exception as e:
|
|
133
|
+
ctx.error(f"Failed to list secrets: {e}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@main.command()
|
|
137
|
+
@click.argument("name")
|
|
138
|
+
@pass_context
|
|
139
|
+
def exists(ctx, name: str):
|
|
140
|
+
"""Check if a secret exists.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
secretctl exists API_KEY
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
value = ctx.backend.get(name)
|
|
147
|
+
exists = value is not None
|
|
148
|
+
ctx.output({"name": name, "exists": exists})
|
|
149
|
+
except Exception as e:
|
|
150
|
+
ctx.error(f"Failed to check secret: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@main.command()
|
|
154
|
+
@click.option("--file", "-f", "output_file", help="Output file (default: stdout)")
|
|
155
|
+
@click.option("--format", "fmt", type=click.Choice(["env", "json"]), default="env", help="Export format")
|
|
156
|
+
@pass_context
|
|
157
|
+
def export(ctx, output_file: str | None, fmt: str):
|
|
158
|
+
"""Export all secrets.
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
secretctl export > secrets.env
|
|
162
|
+
secretctl export --format json > secrets.json
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
secrets = ctx.backend.list()
|
|
166
|
+
data = {}
|
|
167
|
+
for name in secrets:
|
|
168
|
+
value = ctx.backend.get(name)
|
|
169
|
+
if value:
|
|
170
|
+
data[name] = value
|
|
171
|
+
|
|
172
|
+
if fmt == "json":
|
|
173
|
+
output = json.dumps(data, indent=2)
|
|
174
|
+
else: # env format
|
|
175
|
+
output = "\n".join(f'{k}="{v}"' for k, v in data.items())
|
|
176
|
+
|
|
177
|
+
if output_file:
|
|
178
|
+
with open(output_file, "w") as f:
|
|
179
|
+
f.write(output)
|
|
180
|
+
ctx.output({"exported": len(data), "file": output_file, "format": fmt})
|
|
181
|
+
else:
|
|
182
|
+
click.echo(output)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
ctx.error(f"Failed to export secrets: {e}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@main.command("import")
|
|
188
|
+
@click.argument("input_file", type=click.Path(exists=True))
|
|
189
|
+
@click.option("--format", "fmt", type=click.Choice(["env", "json"]), default="env", help="Import format")
|
|
190
|
+
@click.option("--overwrite", is_flag=True, help="Overwrite existing secrets")
|
|
191
|
+
@pass_context
|
|
192
|
+
def import_secrets(ctx, input_file: str, fmt: str, overwrite: bool):
|
|
193
|
+
"""Import secrets from file.
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
secretctl import secrets.env
|
|
197
|
+
secretctl import secrets.json --format json --overwrite
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
with open(input_file, "r") as f:
|
|
201
|
+
content = f.read()
|
|
202
|
+
|
|
203
|
+
if fmt == "json":
|
|
204
|
+
data = json.loads(content)
|
|
205
|
+
else: # env format
|
|
206
|
+
data = {}
|
|
207
|
+
for line in content.splitlines():
|
|
208
|
+
line = line.strip()
|
|
209
|
+
if line and not line.startswith("#") and "=" in line:
|
|
210
|
+
key, value = line.split("=", 1)
|
|
211
|
+
# Strip quotes if present
|
|
212
|
+
value = value.strip().strip('"').strip("'")
|
|
213
|
+
data[key.strip()] = value
|
|
214
|
+
|
|
215
|
+
imported = 0
|
|
216
|
+
skipped = 0
|
|
217
|
+
for name, value in data.items():
|
|
218
|
+
if not overwrite and ctx.backend.get(name) is not None:
|
|
219
|
+
skipped += 1
|
|
220
|
+
continue
|
|
221
|
+
ctx.backend.set(name, value)
|
|
222
|
+
imported += 1
|
|
223
|
+
|
|
224
|
+
ctx.output({"imported": imported, "skipped": skipped, "file": input_file})
|
|
225
|
+
except Exception as e:
|
|
226
|
+
ctx.error(f"Failed to import secrets: {e}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: secretctl-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cross-platform secret storage CLI for AI agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/secretctl
|
|
6
|
+
Project-URL: Repository, https://github.com/marcusbuildsthings-droid/secretctl
|
|
7
|
+
Author-email: Marcus <marcus.builds.things@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agent,cli,credentials,keychain,secrets
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: click>=8.0
|
|
23
|
+
Requires-Dist: rich>=13.0
|
|
24
|
+
Provides-Extra: linux
|
|
25
|
+
Requires-Dist: keyring>=24.0; extra == 'linux'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# secretctl
|
|
29
|
+
|
|
30
|
+
Cross-platform secret storage CLI for AI agents and developers. Store secrets in the OS keychain without cloud sync or biometrics.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install secretctl
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
On Linux, install with keyring support:
|
|
39
|
+
```bash
|
|
40
|
+
pip install secretctl[linux]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Store a secret
|
|
47
|
+
secretctl set API_KEY sk-xxx123
|
|
48
|
+
|
|
49
|
+
# Retrieve a secret
|
|
50
|
+
secretctl get API_KEY
|
|
51
|
+
|
|
52
|
+
# List all secrets
|
|
53
|
+
secretctl list
|
|
54
|
+
|
|
55
|
+
# Delete a secret
|
|
56
|
+
secretctl delete API_KEY
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
### Basic Operations
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
secretctl set <name> <value> # Store a secret
|
|
65
|
+
secretctl get <name> # Retrieve a secret
|
|
66
|
+
secretctl delete <name> # Delete a secret
|
|
67
|
+
secretctl list # List all secret names
|
|
68
|
+
secretctl exists <name> # Check if secret exists
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Import/Export
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Export to env file
|
|
75
|
+
secretctl export > secrets.env
|
|
76
|
+
secretctl export --format json > secrets.json
|
|
77
|
+
|
|
78
|
+
# Import from file
|
|
79
|
+
secretctl import secrets.env
|
|
80
|
+
secretctl import secrets.json --format json
|
|
81
|
+
secretctl import secrets.env --overwrite # Replace existing
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Namespaces
|
|
85
|
+
|
|
86
|
+
Use different accounts/namespaces to organize secrets:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
secretctl --account myapp set DB_URL "postgres://..."
|
|
90
|
+
secretctl --account myapp list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## JSON Output
|
|
94
|
+
|
|
95
|
+
All commands support `--json` for machine-readable output:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
secretctl --json get API_KEY
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"name": "API_KEY",
|
|
104
|
+
"value": "sk-xxx123",
|
|
105
|
+
"success": true
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Platform Support
|
|
110
|
+
|
|
111
|
+
| Platform | Backend | Notes |
|
|
112
|
+
|----------|---------|-------|
|
|
113
|
+
| macOS | Keychain | Uses `security` command |
|
|
114
|
+
| Linux | keyring | Requires `secretctl[linux]` |
|
|
115
|
+
|
|
116
|
+
## For AI Agents
|
|
117
|
+
|
|
118
|
+
See [SKILL.md](SKILL.md) for agent-optimized documentation.
|
|
119
|
+
|
|
120
|
+
## Why secretctl?
|
|
121
|
+
|
|
122
|
+
- **No cloud sync** - Secrets stay local
|
|
123
|
+
- **No biometrics** - Works in automation/CI
|
|
124
|
+
- **Simple CLI** - Easy for agents to use
|
|
125
|
+
- **JSON output** - Machine-readable
|
|
126
|
+
- **Namespaced** - Multiple accounts/apps
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
secretctl/__init__.py,sha256=tk1IHS0BHdVrKCQFraLrU7t8Av-rGOfG3JMneM1KAp4,90
|
|
2
|
+
secretctl/backend.py,sha256=Sz4O_wrK-GrEgoIpH5cz9LaWKlbJf9wSk-bYmCyKdLs,5409
|
|
3
|
+
secretctl/cli.py,sha256=N5Rc8HYnfnyV-TmnJGlezWitFJtTlMvdJvcbZ8mWe4A,6833
|
|
4
|
+
secretctl_cli-0.1.0.dist-info/METADATA,sha256=ysNLdte-0x5NUrbNbK3Pk21YOXCt75GLIn7DfzjLCKg,3019
|
|
5
|
+
secretctl_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
secretctl_cli-0.1.0.dist-info/entry_points.txt,sha256=piuuz-chGYfMG9c1FhvDPP4dVZ_d2doAPcq_e7O6i5U,49
|
|
7
|
+
secretctl_cli-0.1.0.dist-info/licenses/LICENSE,sha256=9tNBpWq8KGbuJqmeComp40OiNnbvpvsKn1YP26PUtck,1063
|
|
8
|
+
secretctl_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|