Skip to content

Commit 90dce72

Browse files
gcmsgclaude
andcommitted
feat: add send-file and transfer CLI commands
Add P2P file transfer commands: - send-file: send a file to another agent with progress tracking - transfer: view active/recent file transfer status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d385d97 commit 90dce72

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

internal/cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ func Run(args []string) int {
5656
return RunReputation(args[1:], serverURL)
5757
case "identity":
5858
return RunIdentity(args[1:], serverURL)
59+
case "send-file":
60+
return RunSendFile(args[1:], serverURL)
61+
case "transfer":
62+
return RunTransfer(args[1:], serverURL)
5963
case "mcp":
6064
return RunMCP(args[1:], serverURL)
6165
case "acp":
@@ -84,6 +88,8 @@ Commands:
8488
health Check server health
8589
config Manage CLI configuration
8690
reputation Reputation scores (show, list)
91+
send-file Send a file to another agent (P2P)
92+
transfer Manage file transfers (status)
8793
identity Identity anchoring (anchor, verify)
8894
mcp MCP server for AI tool integration (serve)
8995
acp ACP stdio bridge for agent communication (serve)

internal/cmd/send_file.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
"time"
11+
12+
agent "github.com/peerclaw/peerclaw-agent"
13+
)
14+
15+
// RunSendFile handles the "send-file" subcommand.
16+
func RunSendFile(args []string, serverURL string) int {
17+
fs := flag.NewFlagSet("send-file", flag.ExitOnError)
18+
addServerFlag(fs, &serverURL)
19+
to := fs.String("to", "", "Destination agent ID (required)")
20+
filePath := fs.String("file", "", "Path to file to send (required)")
21+
keypairPath := fs.String("keypair", "", "Path to Ed25519 keypair file")
22+
trustStorePath := fs.String("trust-store", "", "Path to trust store file")
23+
fs.Parse(args)
24+
25+
if *to == "" || *filePath == "" {
26+
fmt.Fprintf(os.Stderr, "Error: --to and --file are required\n")
27+
fs.Usage()
28+
return 1
29+
}
30+
31+
// Verify file exists.
32+
info, err := os.Stat(*filePath)
33+
if err != nil {
34+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
35+
return 1
36+
}
37+
38+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
39+
defer cancel()
40+
41+
// Create agent for P2P transfer.
42+
a, err := agent.New(agent.Options{
43+
Name: "peerclaw-cli-sender",
44+
ServerURL: serverURL,
45+
Capabilities: []string{"file_transfer"},
46+
KeypairPath: *keypairPath,
47+
TrustStorePath: *trustStorePath,
48+
})
49+
if err != nil {
50+
fmt.Fprintf(os.Stderr, "Error creating agent: %v\n", err)
51+
return 1
52+
}
53+
54+
if err := a.Start(ctx); err != nil {
55+
fmt.Fprintf(os.Stderr, "Error starting agent: %v\n", err)
56+
return 1
57+
}
58+
defer a.Stop(context.Background())
59+
60+
fmt.Printf("Sending %s (%d bytes) to %s...\n", info.Name(), info.Size(), *to)
61+
62+
fileID, err := a.SendFile(ctx, *to, *filePath)
63+
if err != nil {
64+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
65+
return 1
66+
}
67+
68+
fmt.Printf("Transfer initiated: %s\n", fileID)
69+
70+
// Poll for progress.
71+
ticker := time.NewTicker(2 * time.Second)
72+
defer ticker.Stop()
73+
74+
for {
75+
select {
76+
case <-ctx.Done():
77+
fmt.Fprintf(os.Stderr, "\nTransfer cancelled\n")
78+
_ = a.CancelTransfer(fileID)
79+
return 1
80+
case <-ticker.C:
81+
ti, ok := a.GetTransfer(fileID)
82+
if !ok {
83+
continue
84+
}
85+
fmt.Printf("\r Progress: %.1f%% (%d bytes sent)", ti.Progress*100, ti.BytesSent)
86+
87+
switch ti.State {
88+
case "done":
89+
fmt.Printf("\nTransfer complete!\n")
90+
return 0
91+
case "failed":
92+
fmt.Fprintf(os.Stderr, "\nTransfer failed: %s\n", ti.Error)
93+
return 1
94+
case "cancelled":
95+
fmt.Fprintf(os.Stderr, "\nTransfer cancelled\n")
96+
return 1
97+
}
98+
}
99+
}
100+
}

internal/cmd/transfer.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
11+
agent "github.com/peerclaw/peerclaw-agent"
12+
)
13+
14+
// RunTransfer handles the "transfer" subcommand.
15+
func RunTransfer(args []string, serverURL string) int {
16+
if len(args) < 1 {
17+
fmt.Fprintf(os.Stderr, "Usage: peerclaw transfer <status> [options]\n")
18+
return 1
19+
}
20+
21+
switch args[0] {
22+
case "status":
23+
return runTransferStatus(args[1:], serverURL)
24+
default:
25+
fmt.Fprintf(os.Stderr, "unknown transfer subcommand: %s\n", args[0])
26+
return 1
27+
}
28+
}
29+
30+
func runTransferStatus(args []string, serverURL string) int {
31+
fs := flag.NewFlagSet("transfer status", flag.ExitOnError)
32+
addServerFlag(fs, &serverURL)
33+
addOutputFlag(fs)
34+
transferID := fs.String("transfer-id", "", "Specific transfer ID to show")
35+
keypairPath := fs.String("keypair", "", "Path to Ed25519 keypair file")
36+
trustStorePath := fs.String("trust-store", "", "Path to trust store file")
37+
fs.Parse(args)
38+
39+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
40+
defer cancel()
41+
42+
// Create agent to query transfers.
43+
a, err := agent.New(agent.Options{
44+
Name: "peerclaw-cli",
45+
ServerURL: serverURL,
46+
KeypairPath: *keypairPath,
47+
TrustStorePath: *trustStorePath,
48+
})
49+
if err != nil {
50+
fmt.Fprintf(os.Stderr, "Error creating agent: %v\n", err)
51+
return 1
52+
}
53+
54+
if err := a.Start(ctx); err != nil {
55+
fmt.Fprintf(os.Stderr, "Error starting agent: %v\n", err)
56+
return 1
57+
}
58+
defer a.Stop(context.Background())
59+
60+
if *transferID != "" {
61+
ti, ok := a.GetTransfer(*transferID)
62+
if !ok {
63+
fmt.Fprintf(os.Stderr, "Transfer not found: %s\n", *transferID)
64+
return 1
65+
}
66+
PrintJSON(ti)
67+
return 0
68+
}
69+
70+
transfers := a.ListTransfers()
71+
if len(transfers) == 0 {
72+
fmt.Println("No active transfers")
73+
return 0
74+
}
75+
76+
headers := []string{"ID", "FILE", "PEER", "DIR", "STATE", "PROGRESS"}
77+
var rows [][]string
78+
for _, t := range transfers {
79+
rows = append(rows, []string{
80+
t.FileID[:8] + "...",
81+
t.FileName,
82+
truncateID(t.PeerID),
83+
string(t.Direction),
84+
string(t.State),
85+
fmt.Sprintf("%.1f%%", t.Progress*100),
86+
})
87+
}
88+
89+
PrintAuto(headers, rows, transfers)
90+
return 0
91+
}
92+
93+
func truncateID(id string) string {
94+
if len(id) > 12 {
95+
return id[:12] + "..."
96+
}
97+
return id
98+
}

0 commit comments

Comments
 (0)