@pippit-dev/cli 0.0.11 → 0.0.13

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
@@ -1,4 +1,4 @@
1
- # pippit-cli
1
+ # pippit-tool-cli
2
2
 
3
3
  Minimal demo CLI for Pippit workflows.
4
4
 
@@ -10,11 +10,11 @@ matching prebuilt binary for macOS, Linux, or Windows:
10
10
  ```bash
11
11
  npx @pippit-dev/cli@latest install
12
12
  export XYQ_ACCESS_KEY="<access-key>"
13
- pippit-cli --version
14
- pippit-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
15
- pippit-cli short-drama +upload-file --path ./story.md
16
- pippit-cli short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
17
- pippit-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
13
+ pippit-tool-cli --version
14
+ pippit-tool-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
15
+ pippit-tool-cli short-drama +upload-file --path ./reference.doc
16
+ pippit-tool-cli short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
17
+ pippit-tool-cli short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
18
18
  ```
19
19
 
20
20
  NPM package names must be lowercase, so the publishable package name is
@@ -26,7 +26,7 @@ Submit a Run task for the short drama scene:
26
26
  export XYQ_ACCESS_KEY="<access-key>"
27
27
  go run . --version
28
28
  go run . short-drama +submit-run --message "写一个赛博朋克短剧开头"
29
- go run . short-drama +upload-file --path ./story.md
29
+ go run . short-drama +upload-file --path ./reference.doc
30
30
  go run . short-drama +get-thread --thread-id thread_123 --run-id run_456 --after-seq 0
31
31
  go run . short-drama +download-result --output-path ./thread_123/results/result.mp4 --url URL
32
32
  ```
@@ -35,9 +35,11 @@ go run . short-drama +download-result --output-path ./thread_123/results/result.
35
35
  `run_id`, and `web_thread_link`; `--message` is required.
36
36
  `+get-thread` calls
37
37
  `/api/biz/v1/skill/get_thread` and prints extracted `messages`.
38
+ `+upload-file` calls `/api/biz/v1/skill/upload_file` with
39
+ `multipart/form-data` and prints the returned `asset_id`.
40
+ Only `.doc` and `.txt` file extensions are supported.
38
41
  `+download-result` downloads the result URL to
39
- the `--output-path` file path. `+upload-file` is still mocked while
40
- its real service contract is wired.
42
+ the `--output-path` file path.
41
43
 
42
44
  ## HTTP Client
43
45
 
@@ -47,6 +49,6 @@ settings such as base URL, HTTP timeout, and API paths are loaded by
47
49
 
48
50
  ## Auth
49
51
 
50
- `short-drama +submit-run` and `short-drama +get-thread` authenticate with
52
+ `short-drama +submit-run`, `short-drama +get-thread`, and `short-drama +upload-file` authenticate with
51
53
  `Authorization: Bearer <XYQ_ACCESS_KEY>`. OAuth command code remains available,
52
54
  but runtime short drama requests do not use it.
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- ebff380937bb2269a116272d4bb0c5aaef0a176c2ffcfb6f337733411db34ddc pippit-cli-0.0.11-darwin-amd64.tar.gz
2
- de27b844d0df2b112a2a78a24184d023cdf72b145d969497dd351924348161e4 pippit-cli-0.0.11-darwin-arm64.tar.gz
3
- 65da00716f772d3bbb823e10f36e87b73810c8d6f76d5f0c648274d43657f634 pippit-cli-0.0.11-linux-amd64.tar.gz
4
- 420dc63f6ea642c20b0a22f666c652d633d4ddad274b85096b51f74602e85a81 pippit-cli-0.0.11-linux-arm64.tar.gz
5
- 06350f1d74f4130bb420c237e5f804b4df1f358dfc195b659a70ddfd2d48832b pippit-cli-0.0.11-windows-amd64.zip
6
- 6919d1de3bec32499379bbcc820ccf3bafb3e843fdfa4cb9e826f90fae00feb2 pippit-cli-0.0.11-windows-arm64.zip
1
+ c02c37229ea76195c04881880a9d5a913ab95846958ab4e4ab41bc2b82649e49 pippit-tool-cli-0.0.13-darwin-amd64.tar.gz
2
+ 4937231063eeaad1e494be4f2b6218860313abbf529d868568d9d6e48bf62e3e pippit-tool-cli-0.0.13-darwin-arm64.tar.gz
3
+ da2bb0c5472bdd02a2cc861a17564254abb86fa5c63c44b58c7fbb64211550f1 pippit-tool-cli-0.0.13-linux-amd64.tar.gz
4
+ 476e0907a6983f9360324732411edcd9c2c80f9d903a7974949ef2d09b1f35ba pippit-tool-cli-0.0.13-linux-arm64.tar.gz
5
+ cfe8e5b92b8b6abbf5ed3abc9ce31c1371efcc34698edfa5ee2d8d675d4f7886 pippit-tool-cli-0.0.13-windows-amd64.zip
6
+ edff7af4414b2b080338f96eab5ce9cca2a14d4bd8fc3099a1af7a2dc43109d2 pippit-tool-cli-0.0.13-windows-arm64.zip
package/cmd/root.go CHANGED
@@ -13,7 +13,7 @@ import (
13
13
  "github.com/spf13/cobra"
14
14
  )
15
15
 
16
- // Execute runs the pippit-cli command tree.
16
+ // Execute runs the pippit-tool-cli command tree.
17
17
  func Execute() error {
18
18
  return NewRootCommand(os.Stdout, os.Stderr).Execute()
19
19
  }
@@ -27,7 +27,7 @@ func NewRootCommand(stdout, stderr io.Writer) *cobra.Command {
27
27
 
28
28
  func newRootCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
29
29
  root := &cobra.Command{
30
- Use: "pippit-cli",
30
+ Use: "pippit-tool-cli",
31
31
  Short: "Pippit CLI",
32
32
  Long: "Pippit CLI submits short-drama workflows, downloads generated assets, and updates the installed CLI package.",
33
33
  Version: version.Current(),
@@ -3,7 +3,6 @@ package short_drama
3
3
  import (
4
4
  "fmt"
5
5
  "io"
6
- "path/filepath"
7
6
  "strings"
8
7
 
9
8
  "github.com/Pippit-dev/pippit-cli/internal/common"
@@ -72,8 +71,6 @@ func newShortDramaUploadFileCommand(stdout, stderr io.Writer, runner *common.Run
72
71
  if opts.Path == "" {
73
72
  return fmt.Errorf("--path is required")
74
73
  }
75
- opts.FileName = filepath.Base(opts.Path)
76
- opts.Mock = true
77
74
 
78
75
  result, err := common.UploadFile(cmd.Context(), opts, runner)
79
76
  if err != nil {
@@ -176,23 +176,65 @@ func TestShortDramaSubmitRunRequiresAccessKey(t *testing.T) {
176
176
  }
177
177
 
178
178
  func TestShortDramaUploadFile(t *testing.T) {
179
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180
+ if r.Method != http.MethodPost {
181
+ t.Fatalf("method = %s, want POST", r.Method)
182
+ }
183
+ if r.URL.Path != "/api/biz/v1/skill/upload_file" {
184
+ t.Fatalf("path = %s, want upload_file path", r.URL.Path)
185
+ }
186
+ if r.Header.Get("Authorization") != "Bearer test-token" {
187
+ t.Fatalf("Authorization = %q, want test bearer token", r.Header.Get("Authorization"))
188
+ }
189
+ if err := r.ParseMultipartForm(1 << 20); err != nil {
190
+ t.Fatalf("ParseMultipartForm(): %v", err)
191
+ }
192
+ if got := r.FormValue("accessKey"); got != "" {
193
+ t.Fatalf("accessKey = %q, want empty (auth via header only)", got)
194
+ }
195
+ files := r.MultipartForm.File["file"]
196
+ if len(files) != 1 {
197
+ t.Fatalf("file parts = %d, want 1", len(files))
198
+ }
199
+ if files[0].Filename != "story.txt" {
200
+ t.Fatalf("filename = %q, want story.txt", files[0].Filename)
201
+ }
202
+ if got := files[0].Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
203
+ t.Fatalf("Content-Type = %q, want text/plain; charset=utf-8", got)
204
+ }
205
+ file, err := files[0].Open()
206
+ if err != nil {
207
+ t.Fatalf("Open multipart file: %v", err)
208
+ }
209
+ defer file.Close()
210
+ data, err := io.ReadAll(file)
211
+ if err != nil {
212
+ t.Fatalf("ReadAll multipart file: %v", err)
213
+ }
214
+ if string(data) != "txt-data" {
215
+ t.Fatalf("file content = %q, want txt-data", string(data))
216
+ }
217
+ _, _ = w.Write([]byte(`{"ret":"0","errmsg":"","data":{"pippit_asset_id":"asset_123"}}`))
218
+ }))
219
+ defer server.Close()
220
+
221
+ cwd := chdirTemp(t)
222
+ path := filepath.Join(cwd, "story.txt")
223
+ if err := os.WriteFile(path, []byte("txt-data"), 0o644); err != nil {
224
+ t.Fatalf("WriteFile(): %v", err)
225
+ }
226
+
179
227
  var stdout, stderr bytes.Buffer
180
- root := NewRootCommand(&stdout, &stderr)
181
- root.SetArgs([]string{"short-drama", "+upload-file", "--path", "/tmp/story.md"})
228
+ root := newTestRootCommand(t, &stdout, &stderr, server.URL)
229
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
182
230
 
183
231
  if err := root.Execute(); err != nil {
184
232
  t.Fatalf("Execute() error = %v, stderr = %s", err, stderr.String())
185
233
  }
186
234
 
187
235
  got := decodeJSON(t, stdout.Bytes())
188
- if got["scene"] != "short-drama" {
189
- t.Fatalf("scene = %v, want short-drama", got["scene"])
190
- }
191
- if got["status"] != "uploaded" {
192
- t.Fatalf("status = %v, want uploaded", got["status"])
193
- }
194
- if !strings.HasPrefix(got["file_id"].(string), "file_mock_") {
195
- t.Fatalf("file_id = %v, want mock file id", got["file_id"])
236
+ if got["asset_id"] != "asset_123" {
237
+ t.Fatalf("asset_id = %v, want asset_123", got["asset_id"])
196
238
  }
197
239
  }
198
240
 
@@ -210,6 +252,51 @@ func TestShortDramaUploadFileRequiresPath(t *testing.T) {
210
252
  }
211
253
  }
212
254
 
255
+ func TestShortDramaUploadFileRequiresAccessKey(t *testing.T) {
256
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
257
+ t.Fatal("server should not receive request without access key")
258
+ }))
259
+ defer server.Close()
260
+
261
+ cwd := chdirTemp(t)
262
+ path := filepath.Join(cwd, "story.txt")
263
+ if err := os.WriteFile(path, []byte("txt-data"), 0o644); err != nil {
264
+ t.Fatalf("WriteFile(): %v", err)
265
+ }
266
+
267
+ var stdout, stderr bytes.Buffer
268
+ root := newTestRootCommandWithAccessKey(t, &stdout, &stderr, server.URL, "")
269
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
270
+
271
+ err := root.Execute()
272
+ if err == nil {
273
+ t.Fatal("Execute() error = nil, want access key error")
274
+ }
275
+ if !strings.Contains(err.Error(), "XYQ_ACCESS_KEY is required") {
276
+ t.Fatalf("error = %q, want access key guidance", err)
277
+ }
278
+ }
279
+
280
+ func TestShortDramaUploadFileRejectsUnsupportedFileType(t *testing.T) {
281
+ cwd := chdirTemp(t)
282
+ path := filepath.Join(cwd, "story.png")
283
+ if err := os.WriteFile(path, []byte("png-data"), 0o644); err != nil {
284
+ t.Fatalf("WriteFile(): %v", err)
285
+ }
286
+
287
+ var stdout, stderr bytes.Buffer
288
+ root := NewRootCommand(&stdout, &stderr)
289
+ root.SetArgs([]string{"short-drama", "+upload-file", "--path", path})
290
+
291
+ err := root.Execute()
292
+ if err == nil {
293
+ t.Fatal("Execute() error = nil, want file type validation error")
294
+ }
295
+ if !strings.Contains(err.Error(), "only .doc and .txt uploads are supported") {
296
+ t.Fatalf("error = %q, want unsupported type validation", err)
297
+ }
298
+ }
299
+
213
300
  func TestShortDramaDownloadResult(t *testing.T) {
214
301
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
215
302
  if r.Header.Get("User-Agent") != "Pippit-CLI/1.0" {
@@ -18,7 +18,7 @@ const defaultPackage = "@pippit-dev/cli"
18
18
  func NewCommand(stdout, stderr io.Writer) *cobra.Command {
19
19
  cmd := &cobra.Command{
20
20
  Use: "update",
21
- Short: "Update pippit-cli and bundled skills",
21
+ Short: "Update pippit-tool-cli and bundled skills",
22
22
  RunE: func(cmd *cobra.Command, _ []string) error {
23
23
  return runUpdate(stdout, stderr)
24
24
  },
@@ -32,14 +32,14 @@ func runUpdate(stdout, stderr io.Writer) error {
32
32
  pkg = defaultPackage + "@latest"
33
33
  }
34
34
 
35
- fmt.Fprintf(stderr, "Updating pippit-cli via npm: %s\n", pkg)
35
+ fmt.Fprintf(stderr, "Updating pippit-tool-cli via npm: %s\n", pkg)
36
36
  restore, err := prepareSelfReplace()
37
37
  if err != nil {
38
38
  return fmt.Errorf("prepare self replace: %w", err)
39
39
  }
40
40
  if err := runInheritEnv(stderr, []string{"PIPPIT_CLI_SKIP_SKILLS=1"}, "npm", "install", "-g", pkg); err != nil {
41
41
  restore()
42
- return fmt.Errorf("update pippit-cli: %w", err)
42
+ return fmt.Errorf("update pippit-tool-cli: %w", err)
43
43
  }
44
44
 
45
45
  root, err := globalPackageRoot(defaultPackage)
@@ -47,12 +47,12 @@ func runUpdate(stdout, stderr io.Writer) error {
47
47
  return fmt.Errorf("locate global package: %w", err)
48
48
  }
49
49
 
50
- fmt.Fprintln(stderr, "Updating pippit-cli skills...")
50
+ fmt.Fprintln(stderr, "Updating pippit-tool-cli skills...")
51
51
  if err := installSkills(root, stderr); err != nil {
52
- return fmt.Errorf("update pippit-cli skills: %w", err)
52
+ return fmt.Errorf("update pippit-tool-cli skills: %w", err)
53
53
  }
54
54
 
55
- fmt.Fprintln(stdout, "pippit-cli and skills updated")
55
+ fmt.Fprintln(stdout, "pippit-tool-cli and skills updated")
56
56
  return nil
57
57
  }
58
58
 
@@ -107,7 +107,7 @@ func prepareSelfReplace() (func(), error) {
107
107
  if err != nil {
108
108
  return nil, err
109
109
  }
110
- if filepath.Base(exe) != "pippit-cli.exe" || !strings.Contains(exe, "node_modules") {
110
+ if filepath.Base(exe) != "pippit-tool-cli.exe" || !strings.Contains(exe, "node_modules") {
111
111
  return func() {}, nil
112
112
  }
113
113
  old := exe + ".old"
@@ -5,8 +5,12 @@ import (
5
5
  "context"
6
6
  "fmt"
7
7
  "io"
8
+ "mime/multipart"
8
9
  "net/http"
10
+ "net/textproto"
9
11
  "net/url"
12
+ "os"
13
+ "path/filepath"
10
14
  "reflect"
11
15
  "strings"
12
16
  "time"
@@ -16,12 +20,20 @@ import (
16
20
 
17
21
  type Client interface {
18
22
  SendRequest(ctx context.Context, path string, body any, out any) error
23
+ SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error
19
24
  }
20
25
 
21
26
  type RequestAuthorizer interface {
22
27
  Inject(ctx context.Context, req *http.Request) error
23
28
  }
24
29
 
30
+ type MultipartFile struct {
31
+ FieldName string
32
+ Path string
33
+ FileName string
34
+ ContentType string
35
+ }
36
+
25
37
  type httpClient struct {
26
38
  baseURL string
27
39
  httpClient *http.Client
@@ -100,6 +112,89 @@ func (c *httpClient) SendRequest(ctx context.Context, path string, body any, out
100
112
  return c.do(req, out)
101
113
  }
102
114
 
115
+ func (c *httpClient) SendMultipartRequest(ctx context.Context, path string, fields map[string]string, file MultipartFile, out any) error {
116
+ reqURL, err := c.resolveURL(path, nil)
117
+ if err != nil {
118
+ return err
119
+ }
120
+ if file.FieldName == "" {
121
+ return fmt.Errorf("multipart file field name is required")
122
+ }
123
+ if file.FileName == "" {
124
+ file.FileName = filepath.Base(file.Path)
125
+ }
126
+ if file.ContentType == "" {
127
+ file.ContentType = "application/octet-stream"
128
+ }
129
+
130
+ pr, pw := io.Pipe()
131
+ writer := multipart.NewWriter(pw)
132
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, pr)
133
+ if err != nil {
134
+ _ = pr.Close()
135
+ _ = pw.Close()
136
+ return fmt.Errorf("build POST request: %w", err)
137
+ }
138
+ req.Header.Set("Content-Type", writer.FormDataContentType())
139
+ req.Header.Set("Accept", "application/json")
140
+
141
+ if c.authorizer == nil {
142
+ _ = pr.Close()
143
+ _ = pw.Close()
144
+ return fmt.Errorf("authorized request requires authorizer")
145
+ }
146
+ if err := c.authorizer.Inject(ctx, req); err != nil {
147
+ _ = pr.Close()
148
+ _ = pw.Close()
149
+ return fmt.Errorf("inject auth headers: %w", err)
150
+ }
151
+ c.injectHeaders(req)
152
+
153
+ go func() {
154
+ err := writeMultipartBody(writer, fields, file)
155
+ closeErr := writer.Close()
156
+ if err != nil {
157
+ _ = pw.CloseWithError(err)
158
+ return
159
+ }
160
+ _ = pw.CloseWithError(closeErr)
161
+ }()
162
+
163
+ return c.do(req, out)
164
+ }
165
+
166
+ func writeMultipartBody(writer *multipart.Writer, fields map[string]string, file MultipartFile) error {
167
+ for key, value := range fields {
168
+ if err := writer.WriteField(key, value); err != nil {
169
+ return err
170
+ }
171
+ }
172
+
173
+ f, err := os.Open(file.Path)
174
+ if err != nil {
175
+ return fmt.Errorf("open upload file: %w", err)
176
+ }
177
+ defer f.Close()
178
+
179
+ header := make(textproto.MIMEHeader)
180
+ header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(file.FieldName), escapeQuotes(file.FileName)))
181
+ header.Set("Content-Type", file.ContentType)
182
+ part, err := writer.CreatePart(header)
183
+ if err != nil {
184
+ return err
185
+ }
186
+ if _, err := io.Copy(part, f); err != nil {
187
+ return fmt.Errorf("write upload file: %w", err)
188
+ }
189
+ return nil
190
+ }
191
+
192
+ var quotesReplacer = strings.NewReplacer("\\", "\\\\", `"`, `\"`)
193
+
194
+ func escapeQuotes(s string) string {
195
+ return quotesReplacer.Replace(s)
196
+ }
197
+
103
198
  func (c *httpClient) injectHeaders(req *http.Request) {
104
199
  for k, values := range c.headers {
105
200
  for _, v := range values {
@@ -31,7 +31,7 @@ type DownloadResultError struct {
31
31
  Error string `json:"error"`
32
32
  }
33
33
 
34
- // DownloadResultResponse is the JSON envelope printed by `pippit-cli short-drama +download-result`.
34
+ // DownloadResultResponse is the JSON envelope printed by `pippit-tool-cli short-drama +download-result`.
35
35
  type DownloadResultResponse struct {
36
36
  OutputPath string `json:"output_path"`
37
37
  Downloaded []string `json:"downloaded"`
@@ -24,7 +24,7 @@ type ThreadFile struct {
24
24
  DownloadURL string `json:"download_url"`
25
25
  }
26
26
 
27
- // ListThreadFileResult is the JSON envelope printed by `pippit-cli short-drama +list-thread-file`.
27
+ // ListThreadFileResult is the JSON envelope printed by `pippit-tool-cli short-drama +list-thread-file`.
28
28
  type ListThreadFileResult struct {
29
29
  Files []*ThreadFile `json:"files"`
30
30
  Total int64 `json:"total"`
@@ -2,48 +2,103 @@ package common
2
2
 
3
3
  import (
4
4
  "context"
5
- "crypto/sha1"
6
- "encoding/hex"
7
- "time"
5
+ "fmt"
6
+ "mime"
7
+ "os"
8
+ "path/filepath"
9
+ "strings"
10
+
11
+ "github.com/Pippit-dev/pippit-cli/internal/config"
8
12
  )
9
13
 
10
14
  // UploadFileOptions is the stable command-facing request shape for file upload.
11
15
  type UploadFileOptions struct {
12
16
  Path string `json:"path"`
13
17
  FileName string `json:"file_name"`
14
- Purpose string `json:"purpose"`
15
- Mock bool `json:"mock"`
16
18
  }
17
19
 
18
- // UploadFileResult is the JSON envelope printed by `pippit-cli short-drama +upload-file`.
20
+ // UploadFileResult is the JSON envelope printed by `pippit-tool-cli short-drama +upload-file`.
19
21
  type UploadFileResult struct {
20
- Scene string `json:"scene"`
21
- FileID string `json:"file_id"`
22
- Status string `json:"status"`
23
- Uploaded string `json:"uploaded_at"`
24
- Request UploadFileOptions `json:"request"`
22
+ AssetID string `json:"asset_id"`
23
+ }
24
+
25
+ type uploadFileResponse struct {
26
+ Ret string `json:"ret"`
27
+ Errmsg string `json:"errmsg"`
28
+ SvrTime int64 `json:"svr_time"`
29
+ LogID string `json:"log_id"`
30
+ Data struct {
31
+ PippitAssetID string `json:"pippit_asset_id"`
32
+ AssetID string `json:"asset_id"`
33
+ } `json:"data"`
34
+ }
35
+
36
+ const uploadFileFieldName = "file"
37
+
38
+ var allowedUploadExtensions = map[string]bool{
39
+ ".doc": true,
40
+ ".txt": true,
25
41
  }
26
42
 
27
- func UploadFile(ctx context.Context, opts UploadFileOptions, _ *Runner) (*UploadFileResult, error) {
43
+ func UploadFile(ctx context.Context, opts UploadFileOptions, runner *Runner) (*UploadFileResult, error) {
28
44
  if err := ctx.Err(); err != nil {
29
45
  return nil, err
30
46
  }
31
- key := opts.Purpose + "\x00" + opts.FileName + "\x00" + opts.Path
32
- id := stableID(key, 10)
47
+ if runner == nil || runner.Client == nil {
48
+ return nil, fmt.Errorf("upload_file runner client is required")
49
+ }
50
+
51
+ path := strings.TrimSpace(opts.Path)
52
+ if path == "" {
53
+ return nil, fmt.Errorf("upload file path is required")
54
+ }
55
+ info, err := os.Stat(path)
56
+ if err != nil {
57
+ return nil, fmt.Errorf("stat upload file: %w", err)
58
+ }
59
+ if info.IsDir() {
60
+ return nil, fmt.Errorf("upload path %q is a directory", path)
61
+ }
62
+
63
+ ext := strings.ToLower(filepath.Ext(path))
64
+ if !allowedUploadExtensions[ext] {
65
+ return nil, fmt.Errorf("unsupported file extension %q; only .doc and .txt uploads are supported", ext)
66
+ }
67
+ fileName := filepath.Base(path)
68
+ contentType := mime.TypeByExtension(ext)
69
+
70
+ var resp uploadFileResponse
71
+ if err := runner.Client.SendMultipartRequest(ctx, uploadFilePath(runner), nil, MultipartFile{
72
+ FieldName: uploadFileFieldName,
73
+ Path: path,
74
+ FileName: fileName,
75
+ ContentType: contentType,
76
+ }, &resp); err != nil {
77
+ return nil, fmt.Errorf("upload_file request failed: %w", err)
78
+ }
79
+ if resp.Ret != "0" {
80
+ if resp.Errmsg == "" {
81
+ resp.Errmsg = "unknown error"
82
+ }
83
+ return nil, fmt.Errorf("upload_file failed: ret=%s errmsg=%s", resp.Ret, resp.Errmsg)
84
+ }
85
+
86
+ assetID := strings.TrimSpace(resp.Data.PippitAssetID)
87
+ if assetID == "" {
88
+ assetID = strings.TrimSpace(resp.Data.AssetID)
89
+ }
90
+ if assetID == "" {
91
+ return nil, fmt.Errorf("upload_file response missing pippit_asset_id")
92
+ }
93
+
33
94
  return &UploadFileResult{
34
- Scene: "short-drama",
35
- FileID: "file_mock_" + id,
36
- Status: "uploaded",
37
- Uploaded: time.Now().UTC().Format(time.RFC3339),
38
- Request: opts,
95
+ AssetID: assetID,
39
96
  }, nil
40
97
  }
41
98
 
42
- func stableID(key string, n int) string {
43
- sum := sha1.Sum([]byte(key))
44
- id := hex.EncodeToString(sum[:])
45
- if n > len(id) {
46
- return id
99
+ func uploadFilePath(runner *Runner) string {
100
+ if runner != nil && runner.Config != nil && runner.Config.Paths != nil && runner.Config.Paths.UploadFile != "" {
101
+ return runner.Config.Paths.UploadFile
47
102
  }
48
- return id[:n]
103
+ return config.UploadFilePath
49
104
  }
@@ -22,7 +22,7 @@ type ThreadEntry struct {
22
22
  Content []any `json:"content"`
23
23
  }
24
24
 
25
- // GetThreadResult is the JSON envelope printed by `pippit-cli short-drama +get-thread`.
25
+ // GetThreadResult is the JSON envelope printed by `pippit-tool-cli short-drama +get-thread`.
26
26
  type GetThreadResult struct {
27
27
  Messages []*ThreadEntry `json:"messages"`
28
28
  }
@@ -15,7 +15,7 @@ type SubmitRunOptions struct {
15
15
  AssetIDs []string `json:"asset_ids,omitempty"`
16
16
  }
17
17
 
18
- // SubmitRunResult is the JSON envelope printed by `pippit-cli short-drama +submit-run`.
18
+ // SubmitRunResult is the JSON envelope printed by `pippit-tool-cli short-drama +submit-run`.
19
19
  type SubmitRunResult struct {
20
20
  ThreadID string `json:"thread_id"`
21
21
  RunID string `json:"run_id"`
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@pippit-dev/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Pippit CLI",
5
5
  "bin": {
6
- "pippit-cli": "scripts/run.js"
6
+ "pippit-tool-cli": "scripts/run.js"
7
7
  },
8
8
  "scripts": {
9
9
  "postinstall": "node scripts/install.js",
@@ -17,12 +17,12 @@ function getGloballyInstalledVersion() {
17
17
  }
18
18
  }
19
19
 
20
- function whichPippitCli() {
20
+ function whichPippitToolCli() {
21
21
  try {
22
22
  const prefix = runSilent("npm", ["prefix", "-g"], { timeout: 15000 }).toString().trim();
23
23
  const bin = isWindows
24
- ? path.join(prefix, "pippit-cli.cmd")
25
- : path.join(prefix, "bin", "pippit-cli");
24
+ ? path.join(prefix, "pippit-tool-cli.cmd")
25
+ : path.join(prefix, "bin", "pippit-tool-cli");
26
26
  if (fs.existsSync(bin)) return bin;
27
27
  } catch (_) {
28
28
  // Fall back to PATH lookup.
@@ -30,7 +30,7 @@ function whichPippitCli() {
30
30
 
31
31
  try {
32
32
  const cmd = isWindows ? "where" : "which";
33
- return runSilent(cmd, ["pippit-cli"]).toString().split("\n")[0].trim();
33
+ return runSilent(cmd, ["pippit-tool-cli"]).toString().split("\n")[0].trim();
34
34
  } catch (_) {
35
35
  return null;
36
36
  }
@@ -39,7 +39,7 @@ function whichPippitCli() {
39
39
  function main() {
40
40
  const installed = getGloballyInstalledVersion();
41
41
  if (installed) {
42
- console.log(`Updating global pippit-cli (${installed}) via ${PKG}...`);
42
+ console.log(`Updating global pippit-tool-cli (${installed}) via ${PKG}...`);
43
43
  } else {
44
44
  console.log(`Installing ${PKG} globally...`);
45
45
  }
@@ -48,7 +48,7 @@ function main() {
48
48
  env: { ...process.env, PIPPIT_CLI_SKIP_SKILLS: "1" },
49
49
  });
50
50
 
51
- console.log("Installing pippit-cli skills...");
51
+ console.log("Installing pippit-tool-cli skills...");
52
52
  try {
53
53
  installGlobalPackageSkills(DEFAULT_PKG);
54
54
  } catch (err) {
@@ -60,15 +60,15 @@ function main() {
60
60
  installGlobalPackageSkills(DEFAULT_PKG);
61
61
  }
62
62
 
63
- const bin = whichPippitCli();
63
+ const bin = whichPippitToolCli();
64
64
  if (!bin) {
65
- console.error("pippit-cli was installed, but no global command was found in npm prefix.");
65
+ console.error("pippit-tool-cli was installed, but no global command was found in npm prefix.");
66
66
  console.error("Check that npm's global bin directory is in PATH.");
67
67
  process.exit(1);
68
68
  }
69
69
 
70
- console.log(`pippit-cli is ready: ${bin}`);
71
- console.log("Try: pippit-cli short-drama +submit-run --message \"写一个短剧开头\"");
70
+ console.log(`pippit-tool-cli is ready: ${bin}`);
71
+ console.log("Try: pippit-tool-cli short-drama +submit-run --message \"写一个短剧开头\"");
72
72
  }
73
73
 
74
74
  main();
@@ -9,7 +9,7 @@ const { installSkillsFromRoot } = require("./skills");
9
9
 
10
10
  const VERSION = require("../package.json").version.replace(/-.*$/, "");
11
11
  const REPO = "Pippit-dev/cli";
12
- const NAME = "pippit-cli";
12
+ const NAME = "pippit-tool-cli";
13
13
  const ROOT = path.join(__dirname, "..");
14
14
  const BIN_DIR = path.join(ROOT, "bin");
15
15
 
@@ -121,7 +121,7 @@ function install() {
121
121
 
122
122
  fs.mkdirSync(BIN_DIR, { recursive: true });
123
123
 
124
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pippit-cli-"));
124
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pippit-tool-cli-"));
125
125
  const archivePath = path.join(tmpDir, archiveName);
126
126
  try {
127
127
  download(releaseURL, archivePath);
package/scripts/run.js CHANGED
@@ -6,7 +6,7 @@ const path = require("path");
6
6
  const { maybeWarnNewVersion } = require("./version-check");
7
7
 
8
8
  const ext = process.platform === "win32" ? ".exe" : "";
9
- const bin = path.join(__dirname, "..", "bin", "pippit-cli" + ext);
9
+ const bin = path.join(__dirname, "..", "bin", "pippit-tool-cli" + ext);
10
10
  const args = process.argv.slice(2);
11
11
 
12
12
  const oldBin = bin + ".old";
@@ -50,7 +50,7 @@ if (args[0] === "install") {
50
50
  });
51
51
  } catch (_) {
52
52
  console.error(
53
- "\nFailed to prepare pippit-cli binary.\n" +
53
+ "\nFailed to prepare pippit-tool-cli binary.\n" +
54
54
  "Make sure Go is installed and available in PATH, then retry.\n"
55
55
  );
56
56
  process.exit(1);
@@ -85,7 +85,7 @@ function maybeWarnNewVersion(args = [], opts = {}) {
85
85
  if (compareSemver(latest, current) <= 0) return;
86
86
 
87
87
  const warn = opts.warn || console.error;
88
- warn(`[pippit-cli] New version available: ${current} -> ${latest}. Run: pippit-cli update`);
88
+ warn(`[pippit-tool-cli] New version available: ${current} -> ${latest}. Run: pippit-tool-cli update`);
89
89
  }
90
90
 
91
91
  module.exports = {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pippit-short-drama-skill
3
- description: 使用 pippit-cli 的短剧场景能力提交和查询短剧创作任务。覆盖短剧生成、续写、改写、剧情扩展、人物设定、分集草稿、世界观设定、会话文件获取、文件资源下载等创作场景。当用户要求创作短剧、写短剧剧本、续写故事、修改剧情、补充角色设定、查询短剧任务进展、获取短剧会话文件或下载短剧文件资源,或提到 pippit-cli short-drama / 小云雀短剧时触发。
3
+ description: 使用 pippit-tool-cli 的短剧场景能力提交和查询短剧创作任务。覆盖短剧生成、续写、改写、剧情扩展、人物设定、分集草稿、世界观设定、会话文件获取、文件资源下载等创作场景。当用户要求创作短剧、写短剧剧本、续写故事、修改剧情、补充角色设定、查询短剧任务进展、获取短剧会话文件或下载短剧文件资源,或提到 pippit-tool-cli short-drama / 小云雀短剧时触发。
4
4
  user-invocable: true
5
5
  metadata:
6
6
  {
@@ -9,7 +9,7 @@ metadata:
9
9
  "emoji": "📖",
10
10
  "requires":
11
11
  {
12
- "bins": ["pippit-cli"]
12
+ "bins": ["pippit-tool-cli"]
13
13
  }
14
14
  }
15
15
  }
@@ -17,7 +17,7 @@ metadata:
17
17
 
18
18
  # 小云雀短剧创作
19
19
 
20
- 通过 `pippit-cli short-drama` 命令提交短剧创作任务、上传参考文件,并行查询任务进展和会话产物文件,及时把重要资产下载到用户本地。
20
+ 通过 `pippit-tool-cli short-drama` 命令提交短剧创作任务、上传参考文件,并行查询任务进展和会话产物文件,及时把重要资产下载到用户本地。
21
21
 
22
22
  短剧场景面向剧情、人物、分集与画面化叙事创作,用户的原始需求通过 `--message` 发送给后端 Agent。后端 Agent 负责理解任务、编排流程和生成内容;用户侧 Agent 负责提交任务、并行查询进展与产物、主动下载重要资产并展示结果。
23
23
 
@@ -33,7 +33,7 @@ metadata:
33
33
 
34
34
  ## 前置要求
35
35
 
36
- 需要已安装 `pippit-cli`:
36
+ 需要已安装 `pippit-tool-cli`:
37
37
 
38
38
  ```bash
39
39
  npx @pippit-dev/cli@latest install
@@ -45,20 +45,20 @@ npx @pippit-dev/cli@latest install
45
45
 
46
46
  ```bash
47
47
  # 创建新会话并提交短剧创作需求
48
- pippit-cli short-drama +submit-run --message "创作一个赛博朋克短剧开头"
48
+ pippit-tool-cli short-drama +submit-run --message "创作一个赛博朋克短剧开头"
49
49
 
50
50
  # 向已有会话追加新的短剧需求
51
- pippit-cli short-drama +submit-run --message "继续写下一集,重点描写主角的逃亡" --thread-id THREAD_ID
51
+ pippit-tool-cli short-drama +submit-run --message "继续写下一集,重点描写主角的逃亡" --thread-id THREAD_ID
52
52
 
53
53
  # 携带已上传文件 ID 提交任务
54
- pippit-cli short-drama +submit-run --message "参考这个大纲写第一集" --asset-ids ASSET_ID
54
+ pippit-tool-cli short-drama +submit-run --message "参考这个大纲写第一集" --asset-ids ASSET_ID
55
55
  ```
56
56
 
57
57
  ### 2. 查询短剧任务进展
58
58
 
59
59
  ```bash
60
60
  # 查询会话消息列表
61
- pippit-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after-seq 0
61
+ pippit-tool-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after-seq 0
62
62
  ```
63
63
 
64
64
  > `thread_id` 和 `run_id` 由 `+submit-run` 返回。`after-seq` 用于增量拉取消息,首次查询可使用 `0`。
@@ -68,14 +68,14 @@ pippit-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after
68
68
  当用户提供短剧大纲、人物设定、世界观设定、已有分集或剧本等本地文件路径时,可先上传文件。
69
69
 
70
70
  ```bash
71
- pippit-cli short-drama +upload-file --path /path/to/outline.md
71
+ pippit-tool-cli short-drama +upload-file --path /path/to/outline.md
72
72
  ```
73
73
 
74
74
  ### 4. 获取会话文件
75
75
 
76
76
  ```bash
77
77
  # 获取会话文件列表
78
- pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 200
78
+ pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --page-size 200
79
79
  ```
80
80
 
81
81
  `+list-thread-file` 返回的每个文件对象包含:
@@ -93,7 +93,7 @@ pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num 1 --pa
93
93
 
94
94
  ```bash
95
95
  # 下载文件资源到指定文件路径
96
- pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH
96
+ pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH
97
97
  ```
98
98
 
99
99
  `FILE_PATH` 必须直接使用 `+list-thread-file` 返回的完整 `file_path`,包含文件名,不要取父目录。`+download-result` 负责把会话产生的文件通过 URL 下载到该目标文件路径;如果目标文件已存在,跳过下载。
@@ -103,12 +103,12 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PA
103
103
  ### 场景 1:用户要求生成短剧内容
104
104
 
105
105
  ```
106
- 1. pippit-cli short-drama +submit-run --message "用户的原始短剧需求"
106
+ 1. pippit-tool-cli short-drama +submit-run --message "用户的原始短剧需求"
107
107
  → 拿到 thread_id、run_id 和 web_thread_link
108
108
  2. 立即将 web_thread_link 展示给用户
109
109
  3. 并行发起,二者同等重要:
110
- a. pippit-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
111
- b. pippit-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num PAGE_NUM --page-size 200
110
+ a. pippit-tool-cli short-drama +get-thread --thread-id THREAD_ID --run-id RUN_ID --after-seq SEQUENCE
111
+ b. pippit-tool-cli short-drama +list-thread-file --thread-id THREAD_ID --page-num PAGE_NUM --page-size 200
112
112
  4. 检查 `get-thread` 返回的 messages:
113
113
  - 如果任务仍在进行中:展示过程消息,继续查询
114
114
  - 如果后端 Agent 提出问题:展示问题,等待用户回复
@@ -130,9 +130,9 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PA
130
130
  ### 场景 2:用户提供参考文件要求创作
131
131
 
132
132
  ```
133
- 1. pippit-cli short-drama +upload-file --path /path/to/file
133
+ 1. pippit-tool-cli short-drama +upload-file --path /path/to/file
134
134
  → 拿到 file_id
135
- 2. pippit-cli short-drama +submit-run --message "用户的原始短剧需求" --asset-ids file_id
135
+ 2. pippit-tool-cli short-drama +submit-run --message "用户的原始短剧需求" --asset-ids file_id
136
136
  → 拿到 thread_id、run_id 和 web_thread_link
137
137
  3. 后续同场景 1 的并行查询、重要资产发现和文件下载流程
138
138
  ```
@@ -140,7 +140,7 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PA
140
140
  ### 场景 3:在已有短剧会话中续写或修改
141
141
 
142
142
  ```
143
- 1. pippit-cli short-drama +submit-run --message "用户的新需求" --thread-id THREAD_ID
143
+ 1. pippit-tool-cli short-drama +submit-run --message "用户的新需求" --thread-id THREAD_ID
144
144
  → 拿到新的 run_id 和 web_thread_link
145
145
  2. 继续按场景 1 展示进展、处理用户补充问题、获取新增会话文件列表,并及时下载新增重要资产
146
146
  ```
@@ -264,7 +264,7 @@ pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PA
264
264
 
265
265
  对带 `download_url` 的重要资产调用下载工具,可并行。重要资产必须主动下载,不要等用户再次要求,也不要在调用下载工具前先检查本地文件是否存在。
266
266
 
267
- 1. 调用 `pippit-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH`。
267
+ 1. 调用 `pippit-tool-cli short-drama +download-result --url DOWNLOAD_URL --output-path FILE_PATH`。
268
268
  2. 下载完成后,向用户展示本地文件路径;如果某个文件下载失败,记录失败项并在后续轮询中重试,不阻塞已成功落盘的文件展示。
269
269
 
270
270
  ## 向用户展示内容