claude-teammate 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/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/claude-teammate.js +13 -0
- package/package.json +45 -0
- package/src/cli.js +32 -0
- package/src/commands/start.js +189 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Claude Teammate
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
```
|
|
2
|
+
____ _ _ _____ _
|
|
3
|
+
/ ___| | __ _ _ _ __| | ___ |_ _|__ __ _ _ __ ___ _ __ ___ __ _| |_ ___
|
|
4
|
+
| | | |/ _` | | | |/ _` |/ _ \ | |/ _ \/ _` | '_ ` _ \| '_ ` _ \ / _` | __/ _ \
|
|
5
|
+
| |___| | (_| | |_| | (_| | __/ | | __/ (_| | | | | | | | | | | | (_| | || __/
|
|
6
|
+
\____|_|\__,_|\__,_|\__,_|\___| |_|\___|\__,_|_| |_| |_|_| |_| |_|\__,_|\__\___|
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="#quickstart"><strong>Quickstart</strong></a> ·
|
|
11
|
+
<a href="#how-it-works"><strong>How It Works</strong></a> ·
|
|
12
|
+
<a href="#it-remembers"><strong>It Remembers</strong></a> ·
|
|
13
|
+
<a href="https://github.com/ignify-rd/claude-teammate"><strong>GitHub</strong></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<a href="https://github.com/ignify-rd/claude-teammate/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<br/>
|
|
21
|
+
|
|
22
|
+
# An autonomous software engineer that lives in your Jira and GitHub
|
|
23
|
+
|
|
24
|
+
Claude Teammate is an AI bot that works around the clock - picking up tickets, writing plans, opening PRs, acting on feedback, and reviewing code. All without being asked.
|
|
25
|
+
|
|
26
|
+
Assign it tickets. It ships.
|
|
27
|
+
|
|
28
|
+
| | Step | What happens |
|
|
29
|
+
| ------ | ------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| **01** | Assign the ticket | Set the Jira assignee to the bot. |
|
|
31
|
+
| **02** | It plans | Reads the codebase and writes a plan. You give feedback, it revises. Nothing moves until you approve. |
|
|
32
|
+
| **03** | It ships | Opens a PR. Responds to feedback. Keeps going until it's merged. |
|
|
33
|
+
|
|
34
|
+
<br/>
|
|
35
|
+
|
|
36
|
+
## Claude Teammate is right for you if
|
|
37
|
+
|
|
38
|
+
- ✅ You want Jira tickets to **turn into merged PRs** without manual handoff
|
|
39
|
+
- ✅ You want **PR review comments acted on**, not just acknowledged
|
|
40
|
+
- ✅ You need a reviewer that **always shows up** and leaves structured feedback
|
|
41
|
+
- ✅ You want an AI that **remembers** - which repo, what's shipped, what breaks, what to avoid
|
|
42
|
+
- ✅ You want this running on **your own infrastructure**
|
|
43
|
+
|
|
44
|
+
<br/>
|
|
45
|
+
|
|
46
|
+
## How It Works
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
<table>
|
|
50
|
+
<tr>
|
|
51
|
+
<td align="center" width="25%">
|
|
52
|
+
<h3>🎫 Jira → GitHub</h3>
|
|
53
|
+
Picks up assigned Jira issues, asks clarifying questions until it has enough context, then creates a GitHub issue with an implementation plan.
|
|
54
|
+
</td>
|
|
55
|
+
<td align="center" width="25%">
|
|
56
|
+
<h3>⚙️ GitHub Issue → PR</h3>
|
|
57
|
+
When the plan is approved, it implements the issue and opens a pull request. No code is written until you sign off.
|
|
58
|
+
</td>
|
|
59
|
+
<td align="center" width="25%">
|
|
60
|
+
<h3>💬 PR Feedback → Fix</h3>
|
|
61
|
+
Review comments don't sit. The bot reads them, fixes the code, and replies - so the only thing left for you to do is approve.
|
|
62
|
+
</td>
|
|
63
|
+
<td align="center" width="25%">
|
|
64
|
+
<h3>🔍 Review Requests</h3>
|
|
65
|
+
Add it as a reviewer and it will show up - every time, with real feedback, never "LGTM" without reading.
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
</table>
|
|
69
|
+
|
|
70
|
+
<br/>
|
|
71
|
+
|
|
72
|
+
## It Remembers
|
|
73
|
+
|
|
74
|
+
Every epic has its own memory. The bot picks up exactly where it left off - no matter how much time has passed.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
memory/
|
|
78
|
+
└── {domain}/
|
|
79
|
+
└── {workspace}/
|
|
80
|
+
└── epic-{id}.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Field | What it stores |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `repos` | Target GitHub repositories |
|
|
86
|
+
| `status` | Done, in-flight, blocked |
|
|
87
|
+
| `notes` | Architectural decisions, reviewer preferences, conventions |
|
|
88
|
+
| `known issues` | Recurring bugs, fragile areas, patterns to avoid |
|
|
89
|
+
|
|
90
|
+
The bot carries context across tickets, PRs, and sessions.
|
|
91
|
+
|
|
92
|
+
<br/>
|
|
93
|
+
|
|
94
|
+
## Quickstart
|
|
95
|
+
|
|
96
|
+
**Requirements:** Node.js 20+, [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated, a Jira API token, a GitHub PAT with repo + PR permissions.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npx claude-teammate start
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The wizard asks for your Jira and GitHub credentials and writes a `.env` file in the current directory. If the directory is not writable, it exits with a clear error instead of failing silently.
|
|
103
|
+
|
|
104
|
+
```env
|
|
105
|
+
# .env file generated by `npx claude-teammate start`
|
|
106
|
+
JIRA_BASE_URL=https://yourorg.atlassian.net
|
|
107
|
+
JIRA_EMAIL=you@example.com
|
|
108
|
+
JIRA_API_TOKEN=...
|
|
109
|
+
JIRA_BOT_EMAIL=bot@yourorg.com
|
|
110
|
+
GITHUB_PAT=ghp_...
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
<br/>
|
|
114
|
+
|
|
115
|
+
## FAQ
|
|
116
|
+
|
|
117
|
+
**How is this different from just prompting Claude directly?**
|
|
118
|
+
Claude Teammate is a persistent, scheduled process. It doesn't wait for you to type - it polls, acts, and remembers.
|
|
119
|
+
|
|
120
|
+
**What happens if a Jira issue lacks context?**
|
|
121
|
+
It asks. The bot comments on the Jira issue with clarifying questions and waits for answers before doing anything.
|
|
122
|
+
|
|
123
|
+
**Can it handle multiple Jira workspaces and GitHub repos?**
|
|
124
|
+
Yes. Each epic is scoped to its own Jira domain and workspace, and can map to one or more GitHub repositories.
|
|
125
|
+
|
|
126
|
+
**Does it understand the codebase or just generate generic code?**
|
|
127
|
+
It reads the actual repository before writing anything - structure, conventions, existing patterns. The plan it proposes is specific to your code, not a template.
|
|
128
|
+
|
|
129
|
+
<br/>
|
|
130
|
+
|
|
131
|
+
## Roadmap
|
|
132
|
+
|
|
133
|
+
- 🟢 Design the workflow
|
|
134
|
+
- ⚪ Jira integration - read tickets, comment, detect assignee
|
|
135
|
+
- ⚪ GitHub integration - issues, PRs, review requests
|
|
136
|
+
- ⚪ Epic memory - persistent context per epic
|
|
137
|
+
- ⚪ End-to-end: Jira ticket → approved plan → merged PR
|
|
138
|
+
- ⚪ Publish to npm
|
|
139
|
+
|
|
140
|
+
<br/>
|
|
141
|
+
|
|
142
|
+
## Contributing
|
|
143
|
+
|
|
144
|
+
Contributions are welcome. Open an issue to discuss before submitting large changes.
|
|
145
|
+
|
|
146
|
+
<br/>
|
|
147
|
+
|
|
148
|
+
## Community
|
|
149
|
+
|
|
150
|
+
- [GitHub Issues](https://github.com/ignify-rd/claude-teammate/issues) - bugs and feature requests
|
|
151
|
+
- [GitHub Discussions](https://github.com/ignify-rd/claude-teammate/discussions) - ideas and questions
|
|
152
|
+
|
|
153
|
+
<br/>
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT © 2026 Claude Teammate
|
|
158
|
+
|
|
159
|
+
<br/>
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
<p align="center">
|
|
164
|
+
<sub>Open source under MIT. Built for engineers who want to ship, not supervise.</sub>
|
|
165
|
+
</p>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
|
|
5
|
+
import { runCli } from "../src/cli.js";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await runCli(process.argv.slice(2));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
process.stderr.write(`${message}\n`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-teammate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI bootstrapper for Claude Teammate.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"claude-teammate": "bin/claude-teammate.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"start": "node ./bin/claude-teammate.js start"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"cli",
|
|
24
|
+
"automation",
|
|
25
|
+
"jira",
|
|
26
|
+
"github",
|
|
27
|
+
"agent"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/ignify-rd/claude-teammate.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/ignify-rd/claude-teammate/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/ignify-rd/claude-teammate#readme",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^9.23.0",
|
|
42
|
+
"eslint": "^9.23.0",
|
|
43
|
+
"globals": "^16.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { runStartWizard } from "./commands/start.js";
|
|
4
|
+
|
|
5
|
+
const HELP_TEXT = `claude-teammate
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
claude-teammate start
|
|
9
|
+
claude-teammate onboard
|
|
10
|
+
claude-teammate --help
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
start Run the setup wizard and write configuration to .env
|
|
14
|
+
onboard Alias for start
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export async function runCli(args) {
|
|
18
|
+
const [command] = args;
|
|
19
|
+
|
|
20
|
+
if (!command || command === "--help" || command === "-h") {
|
|
21
|
+
process.stdout.write(`${HELP_TEXT}\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (command === "start" || command === "onboard") {
|
|
26
|
+
await runStartWizard();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.stderr.write(`Unknown command: ${command}\n\n${HELP_TEXT}\n`);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
const ENV_PATH = ".env";
|
|
7
|
+
const REQUIRED_FIELDS = [
|
|
8
|
+
{
|
|
9
|
+
key: "JIRA_BASE_URL",
|
|
10
|
+
prompt: "Jira base URL",
|
|
11
|
+
example: "https://yourorg.atlassian.net"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "JIRA_EMAIL",
|
|
15
|
+
prompt: "Jira user email",
|
|
16
|
+
example: "you@example.com"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "JIRA_API_TOKEN",
|
|
20
|
+
prompt: "Jira API token",
|
|
21
|
+
secret: true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "JIRA_BOT_EMAIL",
|
|
25
|
+
prompt: "Bot email used for Jira assignment",
|
|
26
|
+
example: "bot@yourorg.com"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "GITHUB_PAT",
|
|
30
|
+
prompt: "GitHub personal access token",
|
|
31
|
+
secret: true
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export async function runStartWizard() {
|
|
36
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
37
|
+
process.stderr.write("The start wizard requires an interactive terminal.\n");
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existingEnv = await loadExistingEnv();
|
|
43
|
+
const rl = createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
process.stdout.write("Claude Teammate setup\n\n");
|
|
50
|
+
const values = {};
|
|
51
|
+
|
|
52
|
+
for (const field of REQUIRED_FIELDS) {
|
|
53
|
+
const currentValue = existingEnv[field.key];
|
|
54
|
+
values[field.key] = await promptForValue(rl, field, currentValue);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const envContent = buildEnvFile(values);
|
|
58
|
+
await persistEnvFile(envContent);
|
|
59
|
+
|
|
60
|
+
process.stdout.write(`\nSaved configuration to ${ENV_PATH}.\n`);
|
|
61
|
+
process.stdout.write("Claude Teammate runtime is not implemented yet.\n");
|
|
62
|
+
} finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadExistingEnv() {
|
|
68
|
+
try {
|
|
69
|
+
const currentFile = await readFile(ENV_PATH, "utf8");
|
|
70
|
+
return parseEnvFile(currentFile);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Unable to read ${ENV_PATH}: ${formatError(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function promptForValue(rl, field, currentValue) {
|
|
81
|
+
const parts = [field.prompt];
|
|
82
|
+
|
|
83
|
+
if (field.example) {
|
|
84
|
+
parts.push(`example: ${field.example}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (currentValue) {
|
|
88
|
+
parts.push(`current: ${field.secret ? maskSecret(currentValue) : currentValue}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const answer = await rl.question(`${parts.join(" | ")}\n> `);
|
|
92
|
+
const trimmed = answer.trim();
|
|
93
|
+
|
|
94
|
+
if (trimmed) {
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (currentValue) {
|
|
99
|
+
return currentValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`Missing required value for ${field.key}.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildEnvFile(values) {
|
|
106
|
+
const lines = ["# Generated by `claude-teammate start`"];
|
|
107
|
+
|
|
108
|
+
for (const field of REQUIRED_FIELDS) {
|
|
109
|
+
lines.push(`${field.key}=${escapeEnvValue(values[field.key])}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${lines.join("\n")}\n`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function persistEnvFile(content) {
|
|
116
|
+
try {
|
|
117
|
+
try {
|
|
118
|
+
await access(".", fsConstants.W_OK);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Current directory is not writable: ${formatError(error)}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await writeFile(ENV_PATH, content, "utf8");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EACCES") {
|
|
126
|
+
throw new Error(`No permission to write ${ENV_PATH}.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(`Unable to write ${ENV_PATH}: ${formatError(error)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseEnvFile(content) {
|
|
134
|
+
const entries = {};
|
|
135
|
+
|
|
136
|
+
for (const line of content.split(/\r?\n/u)) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
|
|
139
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
144
|
+
if (separatorIndex === -1) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
149
|
+
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
|
150
|
+
entries[key] = stripWrappedQuotes(rawValue);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return entries;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function stripWrappedQuotes(value) {
|
|
157
|
+
if (
|
|
158
|
+
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
159
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
160
|
+
) {
|
|
161
|
+
return value.slice(1, -1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function escapeEnvValue(value) {
|
|
168
|
+
if (/^[A-Za-z0-9_./:-]+$/u.test(value)) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return JSON.stringify(value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function maskSecret(value) {
|
|
176
|
+
if (value.length <= 4) {
|
|
177
|
+
return "*".repeat(value.length);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `${"*".repeat(value.length - 4)}${value.slice(-4)}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatError(error) {
|
|
184
|
+
if (error instanceof Error) {
|
|
185
|
+
return error.message;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return String(error);
|
|
189
|
+
}
|