@pippit-dev/cli 1.0.54 → 1.0.55

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
@@ -77,6 +77,14 @@ pippit-tool-cli get-credit-balance --team-id 0 # 仅本次使用个人
77
77
 
78
78
  `space list/use TEAM_ID` 依赖 API 部署 `/api/biz/v1/skill/get_lite_team_list` 的 AK 适配入口,业务团队使用还依赖对应 API 团队权限实现;不直连 commerce Web 接口。宿主 Agent 的选择、任务恢复与不回退规则已写入三个 Skill 的 `Space Selection` 章节。
79
79
 
80
+ ## 查询出口 IP
81
+
82
+ ```bash
83
+ pippit-tool-cli ip
84
+ ```
85
+
86
+ 输出 `{"client_ip":"..."}`,返回服务端看到的出口 IP,无需登录或 AK;沿用 `config.toml` 的 API 地址与 PPE 配置。AK 的 IP 白名单仍在网页密钥管理页设置。
87
+
80
88
  ## 账号 AIGC 水印设置
81
89
 
82
90
  ```bash
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- 3027db71d4e1b89ccaaa50f4660fb41a90e578bf7c08f0ea1c28367a597371a1 pippit-tool-cli-1.0.54-darwin-amd64.tar.gz
2
- b6a932ae33085d565e88c21a39b446dcb21cf148b6fb66d5a5baa040896d98ae pippit-tool-cli-1.0.54-darwin-arm64.tar.gz
3
- aabce0e36f96a1dcc29c24a277e85859e938f6eddc4ed74a0536df3e4e4bce23 pippit-tool-cli-1.0.54-linux-amd64.tar.gz
4
- beb74396a2d82e05f7199b05cdd7850b148748b18483f9ccf24c4ec09d573968 pippit-tool-cli-1.0.54-linux-arm64.tar.gz
5
- c84ccf75b7c3474f2623f0563c4bf1193a2c84b6f8d0d6ee853ac9c00913ee9c pippit-tool-cli-1.0.54-windows-amd64.zip
6
- 8bd9e6abcde7060fb8b1fac9cb4ea9729cc88b73b27dc9d775581306738a56ba pippit-tool-cli-1.0.54-windows-arm64.zip
1
+ 99a7ec8c86f332f9a3a8e7538d9eb1c237e0d8eebdacdaeb262f77ea716d3bd7 pippit-tool-cli-1.0.55-darwin-amd64.tar.gz
2
+ 8cefadb8e6ac41a7c6ff2f1a1ab89448176734ff449238cbcd3f38bbc0acc8cf pippit-tool-cli-1.0.55-darwin-arm64.tar.gz
3
+ 69d0d83c74c95b46f1eb859ed5a9fe4aaf3b38f25465ff5a4d0c0bf33d815988 pippit-tool-cli-1.0.55-linux-amd64.tar.gz
4
+ 086db5207fbe14e15fb97f0a3c970c36de06e67d333cd68373be6a0e5af2626a pippit-tool-cli-1.0.55-linux-arm64.tar.gz
5
+ 14cda14f712ff2c09566fbeb68f39296b200e3d54cdc659e13baf1e9f9b1ed92 pippit-tool-cli-1.0.55-windows-amd64.zip
6
+ 86ae993c8b32c1a6a09c81882e21bac7e8368468fe28b1e7be90b68466d8fe23 pippit-tool-cli-1.0.55-windows-arm64.zip
package/cmd/ip.go ADDED
@@ -0,0 +1,28 @@
1
+ package cmd
2
+
3
+ import (
4
+ "io"
5
+
6
+ "github.com/Pippit-dev/pippit-cli/internal/adapter"
7
+ "github.com/Pippit-dev/pippit-cli/internal/commandnames"
8
+ "github.com/Pippit-dev/pippit-cli/internal/common"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ func newIPCommand(stdout, stderr io.Writer, runner *common.Runner, policy adapter.HTTPAdapter) *cobra.Command {
13
+ cmd := &cobra.Command{
14
+ Use: commandnames.IP,
15
+ Short: "Query the outbound IP observed by the server",
16
+ Args: cobra.NoArgs,
17
+ RunE: withErrorLog(commandnames.IP, nil, func(cmd *cobra.Command, _ []string) error {
18
+ result, err := common.GetClientIP(cmd.Context(), runner, policy)
19
+ if err != nil {
20
+ return err
21
+ }
22
+ return common.WriteJSON(stdout, result)
23
+ }),
24
+ }
25
+ cmd.SetOut(stdout)
26
+ cmd.SetErr(stderr)
27
+ return cmd
28
+ }
package/cmd/ip_test.go ADDED
@@ -0,0 +1,101 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "io"
9
+ "net/http"
10
+ "net/http/httptest"
11
+ "strings"
12
+ "testing"
13
+ "time"
14
+
15
+ "github.com/Pippit-dev/pippit-cli/internal/adapter"
16
+ "github.com/Pippit-dev/pippit-cli/internal/auth"
17
+ "github.com/Pippit-dev/pippit-cli/internal/common"
18
+ "github.com/Pippit-dev/pippit-cli/internal/config"
19
+ )
20
+
21
+ type ipCredentialStore struct {
22
+ auth.CredentialStore
23
+ loads int
24
+ }
25
+
26
+ func (store *ipCredentialStore) Load(context.Context) (*auth.Credential, error) {
27
+ store.loads++
28
+ return nil, errors.New("corrupt credential fixture")
29
+ }
30
+
31
+ type ipCommandPolicy struct {
32
+ adapter.Default
33
+ options adapter.CLIOptions
34
+ }
35
+
36
+ func (policy ipCommandPolicy) CLIOptions() adapter.CLIOptions { return policy.options }
37
+
38
+ func TestIPCommandDoesNotReadCredentialsOrInitializeSpace(t *testing.T) {
39
+ for _, accessKey := range []string{"", "existing-key"} {
40
+ t.Run("configured_key="+accessKey, func(t *testing.T) {
41
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42
+ for _, header := range []string{"Authorization", "Cookie", "X-Team-Id"} {
43
+ if r.Header.Get(header) != "" {
44
+ t.Errorf("anonymous command sent %s", header)
45
+ }
46
+ }
47
+ _, _ = io.WriteString(w, `{"ret":"0","data":{"client_ip":"8.8.8.8"}}`)
48
+ }))
49
+ defer server.Close()
50
+ cfg := config.Load()
51
+ cfg.BaseURL, cfg.AccessKey = server.URL, accessKey
52
+ cfg.HTTPTimeout = time.Second
53
+ policy := ipCommandPolicy{options: adapter.CLIOptions{
54
+ DefaultAllowRootCommands: true,
55
+ LocalCredentialsEnabled: true,
56
+ }}
57
+ runner := newRootRunnerWithAdapter(cfg, policy)
58
+ store := &ipCredentialStore{}
59
+ runner.Auth = auth.NewManager(cfg, auth.WithCredentialStore(store))
60
+ var stdout, stderr bytes.Buffer
61
+ root := newRootCommandWithAdapter(&stdout, &stderr, runner, policy)
62
+ root.SetArgs([]string{"--team-id", "42", "ip"})
63
+ if err := root.Execute(); err != nil {
64
+ t.Fatalf("Execute() = %v, stderr = %q", err, stderr.String())
65
+ }
66
+ if store.loads != 0 {
67
+ t.Fatalf("credential loads = %d, want 0 even with corrupt credentials", store.loads)
68
+ }
69
+ command, _, err := root.Find([]string{"ip"})
70
+ if err != nil || command == nil || common.SpaceSession(command.Context()) != nil {
71
+ t.Fatalf("ip initialized a space session: %v", err)
72
+ }
73
+ var output map[string]string
74
+ if err := json.Unmarshal(stdout.Bytes(), &output); err != nil || len(output) != 1 || output["client_ip"] != "8.8.8.8" {
75
+ t.Fatalf("stdout = %q, error = %v, want only client_ip", stdout.String(), err)
76
+ }
77
+ })
78
+ }
79
+ }
80
+
81
+ func TestIPCommandRespectsAdapterFiltering(t *testing.T) {
82
+ for _, allowed := range []bool{true, false} {
83
+ options := adapter.CLIOptions{}
84
+ if allowed {
85
+ options.AllowedRootCommands = []string{"ip"}
86
+ }
87
+ root := NewRootCommandWithAdapter(io.Discard, io.Discard, ipCommandPolicy{options: options})
88
+ command, _, _ := root.Find([]string{"ip"})
89
+ if got := command != nil && command.Name() == "ip"; got != allowed {
90
+ t.Fatalf("ip present = %v, want %v", got, allowed)
91
+ }
92
+ }
93
+ }
94
+
95
+ func TestIPCommandRejectsArguments(t *testing.T) {
96
+ command := newIPCommand(io.Discard, io.Discard, nil, adapter.Default{})
97
+ command.SetArgs([]string{"unexpected"})
98
+ if err := command.Execute(); err == nil || !strings.Contains(err.Error(), "unexpected") {
99
+ t.Fatalf("Execute() = %v, want positional argument rejection", err)
100
+ }
101
+ }
package/cmd/root.go CHANGED
@@ -120,6 +120,7 @@ func newRootCommandWithAdapter(stdout, stderr io.Writer, runner *common.Runner,
120
120
  root.AddCommand(newDownloadResultCommand(stdout, stderr, runner))
121
121
  root.AddCommand(newGetCreditBalanceCommand(stdout, stderr, runner))
122
122
  root.AddCommand(newWatermarkCommand(stdout, stderr, runner))
123
+ root.AddCommand(newIPCommand(stdout, stderr, runner, policy))
123
124
  root.AddCommand(newModelCommand(stdout, stderr, runner))
124
125
  root.AddCommand(newMarketingCommand(stdout, stderr, runner, policy))
125
126
  root.AddCommand(newGetThreadCommand(stdout, stderr, runner))
package/cmd/space.go CHANGED
@@ -30,7 +30,7 @@ func prepareCommandSpace(cmd *cobra.Command, runner *common.Runner, stderr io.Wr
30
30
  top = top.Parent()
31
31
  }
32
32
  switch top.Name() {
33
- case commandnames.Space, commandnames.Login, commandnames.Logout, commandnames.Status, commandnames.Update, commandnames.Completion, commandnames.Help, commandnames.DownloadResult, commandnames.Watermark:
33
+ case commandnames.Space, commandnames.Login, commandnames.Logout, commandnames.Status, commandnames.Update, commandnames.Completion, commandnames.Help, commandnames.DownloadResult, commandnames.Watermark, commandnames.IP:
34
34
  return nil
35
35
  }
36
36
  raw, _ := cmd.Flags().GetString("space-context")
@@ -1,6 +1,6 @@
1
- 3027db71d4e1b89ccaaa50f4660fb41a90e578bf7c08f0ea1c28367a597371a1 pippit-tool-cli-1.0.54-darwin-amd64.tar.gz
2
- b6a932ae33085d565e88c21a39b446dcb21cf148b6fb66d5a5baa040896d98ae pippit-tool-cli-1.0.54-darwin-arm64.tar.gz
3
- aabce0e36f96a1dcc29c24a277e85859e938f6eddc4ed74a0536df3e4e4bce23 pippit-tool-cli-1.0.54-linux-amd64.tar.gz
4
- beb74396a2d82e05f7199b05cdd7850b148748b18483f9ccf24c4ec09d573968 pippit-tool-cli-1.0.54-linux-arm64.tar.gz
5
- c84ccf75b7c3474f2623f0563c4bf1193a2c84b6f8d0d6ee853ac9c00913ee9c pippit-tool-cli-1.0.54-windows-amd64.zip
6
- 8bd9e6abcde7060fb8b1fac9cb4ea9729cc88b73b27dc9d775581306738a56ba pippit-tool-cli-1.0.54-windows-arm64.zip
1
+ 99a7ec8c86f332f9a3a8e7538d9eb1c237e0d8eebdacdaeb262f77ea716d3bd7 pippit-tool-cli-1.0.55-darwin-amd64.tar.gz
2
+ 8cefadb8e6ac41a7c6ff2f1a1ab89448176734ff449238cbcd3f38bbc0acc8cf pippit-tool-cli-1.0.55-darwin-arm64.tar.gz
3
+ 69d0d83c74c95b46f1eb859ed5a9fe4aaf3b38f25465ff5a4d0c0bf33d815988 pippit-tool-cli-1.0.55-linux-amd64.tar.gz
4
+ 086db5207fbe14e15fb97f0a3c970c36de06e67d333cd68373be6a0e5af2626a pippit-tool-cli-1.0.55-linux-arm64.tar.gz
5
+ 14cda14f712ff2c09566fbeb68f39296b200e3d54cdc659e13baf1e9f9b1ed92 pippit-tool-cli-1.0.55-windows-amd64.zip
6
+ 86ae993c8b32c1a6a09c81882e21bac7e8368468fe28b1e7be90b68466d8fe23 pippit-tool-cli-1.0.55-windows-arm64.zip
@@ -11,5 +11,6 @@ const (
11
11
  Help = "help"
12
12
  Model = "model"
13
13
  Watermark = "watermark"
14
+ IP = "ip"
14
15
  DownloadResult = "download-result"
15
16
  )
@@ -0,0 +1,58 @@
1
+ package common
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ "net/netip"
8
+
9
+ "github.com/Pippit-dev/pippit-cli/internal/adapter"
10
+ "github.com/Pippit-dev/pippit-cli/internal/config"
11
+ )
12
+
13
+ type ClientIPResult struct {
14
+ ClientIP string `json:"client_ip"`
15
+ }
16
+
17
+ type clientIPRequestPolicy struct {
18
+ adapter.HTTPAdapter
19
+ }
20
+
21
+ func (clientIPRequestPolicy) PrepareAPIRequest(context.Context, *http.Request) (adapter.AuthDecision, error) {
22
+ return adapter.UseDefaultAuth, nil
23
+ }
24
+
25
+ // GetClientIP keeps the host transport and routing, without resolving account credentials.
26
+ func GetClientIP(ctx context.Context, runner *Runner, policy adapter.HTTPAdapter) (*ClientIPResult, error) {
27
+ if runner == nil || runner.Config == nil {
28
+ return nil, fmt.Errorf("查询出口 IP 的运行配置缺失")
29
+ }
30
+ cfg := runner.Config
31
+ anonymousPolicy := clientIPRequestPolicy{HTTPAdapter: adapter.HTTPOrDefault(policy)}
32
+ client := NewHTTPClientWithAdapter(cfg.BaseURL, cfg.HTTPTimeout, NewAccessKeyAuthorizer(""), adapter.WithPPE(anonymousPolicy, cfg.PPEEnv))
33
+ // This query is anonymous even when the caller's context carries a selected team.
34
+ ctx = WithSpace(ctx, nil)
35
+ var resp struct {
36
+ Ret string `json:"ret"`
37
+ Errmsg string `json:"errmsg"`
38
+ LogID string `json:"log_id"`
39
+ Data *ClientIPResult `json:"data"`
40
+ }
41
+ if err := client.SendRequest(ctx, config.GetClientIPPath, nil, &resp); err != nil {
42
+ return nil, fmt.Errorf("查询出口 IP 请求失败: %w", err)
43
+ }
44
+ if resp.Ret != "0" {
45
+ if resp.Errmsg == "" {
46
+ resp.Errmsg = "未知错误"
47
+ }
48
+ return nil, NewLogIDError(fmt.Sprintf("查询出口 IP 请求返回失败: ret=%s errmsg=%s", resp.Ret, resp.Errmsg), resp.LogID)
49
+ }
50
+ if resp.Data == nil || resp.Data.ClientIP == "" {
51
+ return nil, NewLogIDError("查询出口 IP 响应缺少 data.client_ip", resp.LogID)
52
+ }
53
+ ip, err := netip.ParseAddr(resp.Data.ClientIP)
54
+ if err != nil || ip.Zone() != "" {
55
+ return nil, NewLogIDError("查询出口 IP 响应的 data.client_ip 无效", resp.LogID)
56
+ }
57
+ return &ClientIPResult{ClientIP: ip.Unmap().String()}, nil
58
+ }
@@ -0,0 +1,135 @@
1
+ package common
2
+
3
+ import (
4
+ "context"
5
+ "io"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "strings"
9
+ "testing"
10
+ "time"
11
+
12
+ "github.com/Pippit-dev/pippit-cli/internal/adapter"
13
+ "github.com/Pippit-dev/pippit-cli/internal/config"
14
+ "github.com/Pippit-dev/pippit-cli/internal/space"
15
+ )
16
+
17
+ type clientIPPolicy struct {
18
+ adapter.Default
19
+ prepareCalls int
20
+ }
21
+
22
+ func (p *clientIPPolicy) PrepareAPIRequest(_ context.Context, req *http.Request) (adapter.AuthDecision, error) {
23
+ p.prepareCalls++
24
+ req.Header.Set("Authorization", "Bearer adapter-key")
25
+ req.Header.Set("Cookie", "sessionid=adapter-cookie")
26
+ req.Header.Set("X-Team-Id", "99")
27
+ return adapter.AuthProvided, nil
28
+ }
29
+
30
+ func (*clientIPPolicy) WrapTransport(next http.RoundTripper) http.RoundTripper {
31
+ if next == nil {
32
+ next = http.DefaultTransport
33
+ }
34
+ return clientIPTransport{next: next}
35
+ }
36
+
37
+ type clientIPTransport struct{ next http.RoundTripper }
38
+
39
+ func (transport clientIPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
40
+ request := req.Clone(req.Context())
41
+ request.Header.Set("X-Test-IP-Transport", "present")
42
+ return transport.next.RoundTrip(request)
43
+ }
44
+
45
+ func TestGetClientIPIsAnonymousAndPreservesTransportAndRouting(t *testing.T) {
46
+ for _, env := range []string{"", "ppe_first", "ppe_second"} {
47
+ t.Run("env="+env, func(t *testing.T) {
48
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49
+ if r.Method != http.MethodGet || r.URL.Path != "/api/biz/v1/skill/get_client_ip" {
50
+ t.Errorf("request = %s %s, want anonymous client IP GET", r.Method, r.URL.Path)
51
+ }
52
+ body, err := io.ReadAll(r.Body)
53
+ if err != nil || len(body) != 0 || r.Header.Get("Content-Type") != "" {
54
+ t.Errorf("GET body = %q, Content-Type = %q, error = %v", body, r.Header.Get("Content-Type"), err)
55
+ }
56
+ for _, header := range []string{"Authorization", "Cookie", "X-Team-Id"} {
57
+ if r.Header.Get(header) != "" {
58
+ t.Errorf("anonymous request retained %s", header)
59
+ }
60
+ }
61
+ if r.Header.Get("X-Test-IP-Transport") != "present" {
62
+ t.Error("anonymous request bypassed adapter transport")
63
+ }
64
+ wantUsePPE := ""
65
+ if env != "" {
66
+ wantUsePPE = "1"
67
+ }
68
+ if r.Header.Get("x-tt-env") != env || r.Header.Get("x-use-ppe") != wantUsePPE {
69
+ t.Error("anonymous request did not follow the configured PPE environment")
70
+ }
71
+ _, _ = io.WriteString(w, `{"ret":"0","data":{"client_ip":"8.8.8.8"}}`)
72
+ }))
73
+ defer server.Close()
74
+
75
+ cfg := config.Load()
76
+ cfg.BaseURL, cfg.PPEEnv, cfg.AccessKey = server.URL, env, "existing-key"
77
+ cfg.HTTPTimeout = time.Second
78
+ policy := &clientIPPolicy{}
79
+ runner := NewRunner(cfg, NewHTTPClientWithAdapter(server.URL, time.Second, NewAccessKeyAuthorizer(cfg.AccessKey), policy))
80
+ teamID := "42"
81
+ ctx := WithSpace(context.Background(), &space.Session{Explicit: &teamID})
82
+ result, err := GetClientIP(ctx, runner, policy)
83
+ if err != nil || result == nil || result.ClientIP != "8.8.8.8" {
84
+ t.Fatalf("GetClientIP() = %#v, %v", result, err)
85
+ }
86
+ if policy.prepareCalls != 0 {
87
+ t.Fatalf("adapter authentication calls = %d, want 0", policy.prepareCalls)
88
+ }
89
+ })
90
+ }
91
+ }
92
+
93
+ func TestGetClientIPRejectsUnusableResponses(t *testing.T) {
94
+ for _, tc := range []struct {
95
+ name, response, wantError string
96
+ status int
97
+ }{
98
+ {"unavailable_source", `{"ret":"53001","errmsg":"暂时无法识别当前来源 IP","log_id":"log_ip"}`, "53001", http.StatusOK},
99
+ {"unavailable_source_http503", `{"ret":"53001","errmsg":"暂时无法识别当前来源 IP","log_id":"log_ip"}`, "53001", http.StatusServiceUnavailable},
100
+ {"missing_data", `{"ret":"0","log_id":"log_ip"}`, "data", http.StatusOK},
101
+ {"missing_ip", `{"ret":"0","log_id":"log_ip","data":{}}`, "client_ip", http.StatusOK},
102
+ {"invalid_ip", `{"ret":"0","log_id":"log_ip","data":{"client_ip":"not-an-ip"}}`, "client_ip", http.StatusOK},
103
+ {"zoned_ip", `{"ret":"0","log_id":"log_ip","data":{"client_ip":"fe80::1%eth0"}}`, "client_ip", http.StatusOK},
104
+ } {
105
+ t.Run(tc.name, func(t *testing.T) {
106
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
107
+ w.WriteHeader(tc.status)
108
+ _, _ = io.WriteString(w, tc.response)
109
+ }))
110
+ defer server.Close()
111
+ cfg := config.Load()
112
+ cfg.BaseURL = server.URL
113
+ cfg.HTTPTimeout = time.Second
114
+ _, err := GetClientIP(context.Background(), NewRunner(cfg, nil), adapter.Default{})
115
+ if err == nil || !strings.Contains(err.Error(), tc.wantError) || !strings.Contains(err.Error(), "log_ip") {
116
+ t.Fatalf("GetClientIP() error = %v, want %q and log ID", err, tc.wantError)
117
+ }
118
+ })
119
+ }
120
+ }
121
+
122
+ func TestGetClientIPSupportsIPv6(t *testing.T) {
123
+ const ip = "2001:4860:4860::8888"
124
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
125
+ _, _ = io.WriteString(w, `{"ret":"0","data":{"client_ip":"`+ip+`"}}`)
126
+ }))
127
+ defer server.Close()
128
+ cfg := config.Load()
129
+ cfg.BaseURL = server.URL
130
+ cfg.HTTPTimeout = time.Second
131
+ result, err := GetClientIP(context.Background(), NewRunner(cfg, nil), adapter.Default{})
132
+ if err != nil || result == nil || result.ClientIP != ip {
133
+ t.Fatalf("GetClientIP() = %#v, %v, want IPv6 %s", result, err, ip)
134
+ }
135
+ }
@@ -21,6 +21,7 @@ const (
21
21
  SubmitMarketingRunPath = "/api/biz/v1/agent/submit_marketing_run"
22
22
  QueryGenerateVideoResultPath = "/api/biz/v1/agent/query_generate_video_result"
23
23
  GetCreditBalancePath = "/api/biz/v1/skill/get_credit_balance"
24
+ GetClientIPPath = "/api/biz/v1/skill/get_client_ip"
24
25
  GetWatermarkSettingPath = "/api/biz/v1/skill/mget_user_setting"
25
26
  UpdateWatermarkSettingPath = "/api/biz/v1/skill/update_user_setting"
26
27
  GetLiteTeamListPath = "/api/biz/v1/skill/get_lite_team_list"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "1.0.54",
3
+ "version": "1.0.55",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
6
  "pippit-tool-cli": "scripts/run.js"