reallink-cli 0.1.12 → 0.1.14

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
@@ -53,6 +53,7 @@ reallink token create --name "ci-token" --scope trace:read --scope trace:write
53
53
  reallink token revoke --token-id tok_xxx
54
54
 
55
55
  reallink file list --project-id prj_xxx
56
+ reallink file tree --project-id prj_xxx
56
57
  reallink file get --asset-id ast_xxx
57
58
  reallink file stat --asset-id ast_xxx
58
59
  reallink file download --asset-id ast_xxx --output ./downloads/report.json
@@ -60,15 +61,28 @@ reallink file download --asset-id ast_xxx --output ./downloads/report.json --res
60
61
  reallink file upload --project-id prj_xxx --source ./local/report.json --path reports/daily
61
62
  reallink file mkdir --project-id prj_xxx --path reports/archive
62
63
  reallink file move --asset-id ast_xxx --file-name reports/archive/report.json
64
+ reallink file move-folder --project-id prj_xxx --source-path reports/archive --target-path reports/2026
63
65
  reallink file remove --asset-id ast_xxx
64
66
 
67
+ reallink logs status
68
+ reallink logs consent --enable
69
+ reallink logs tail --lines 120
70
+ reallink logs upload --project-id prj_xxx --dry-run
71
+ reallink logs upload --project-id prj_xxx --clear-on-success
72
+
65
73
  reallink tool list
66
74
  reallink tool register --manifest ./spec/tools/trace-placeholder.tool.jsonc
75
+ reallink tool publish --tool-id trace.placeholder --channel ga --visibility public
67
76
  reallink tool enable --tool-id trace.placeholder --project-id prj_xxx
68
77
  reallink tool enable --tool-id trace.placeholder --user-id usr_xxx
78
+ reallink tool context put --context-id aa-main --project-id prj_xxx --text "Build notes and constraints"
79
+ reallink tool context get --context-id aa-main --project-id prj_xxx
80
+ reallink tool prompt --tool-id agent.chat --project-id prj_xxx --prompt "Summarize latest trace findings" --session-key aa-main
69
81
  reallink tool run --tool-id trace.placeholder --project-id prj_xxx --input-file ./run-input.jsonc --idempotency-key run-001
70
82
  reallink tool runs --project-id prj_xxx
71
83
  reallink tool get-run --run-id trun_xxx
84
+ reallink tool run-events --run-id trun_xxx --limit 200
85
+ reallink tool run-events --run-id trun_xxx --stage-prefix workflow. --status succeeded
72
86
 
73
87
  reallink link unreal --project-id prj_xxx --uproject "D:/Games/MyGame/MyGame.uproject" --engine-root "D:/Epic/UE_5.4" --set-default
74
88
  reallink link list
@@ -93,16 +107,32 @@ reallink link plugin install --project-id prj_xxx --name RealLinkUnreal --url ht
93
107
  - If the server rejects `tools:*` as invalid, CLI automatically retries with compatible scopes.
94
108
  - You can always pass explicit scopes via repeated `--scope`.
95
109
 
96
- `tool register`, `tool enable/disable` metadata, and `tool run --input-file` accept JSONC.
110
+ `tool register`, `tool enable/disable` metadata, `tool run --input-file`, and `tool prompt --input-file` accept JSONC.
111
+ `tool context put --text-file` accepts plain UTF-8 text files and stores scoped context via API gateway.
97
112
 
98
113
  ## Session Behavior
99
114
 
100
115
  - Login stores a local session at:
101
116
  - Windows: `%APPDATA%\\reallink\\session.json`
102
117
  - Linux/macOS: `${XDG_CONFIG_HOME:-~/.config}/reallink/session.json`
118
+ - Access token TTL: 15 minutes (auto-refreshed by CLI on 401).
119
+ - Refresh/session TTL: 30 days.
103
120
  - You only need to log in again when the saved session is invalid/expired or you explicitly run `reallink logout`.
104
121
  - `reallink logout` revokes the current server session (`/auth/logout`) and clears the local session file.
105
122
 
123
+ ## CLI Logs & Crash Reports
124
+
125
+ - Local logs are written under:
126
+ - Windows: `%APPDATA%\\reallink\\logs\\`
127
+ - Linux/macOS: `${XDG_CONFIG_HOME:-~/.config}/reallink/logs/`
128
+ - Runtime log file: `runtime.jsonl`
129
+ - Crash reports: `crash/crash-<epoch>.json`
130
+ - Uploading logs requires explicit consent:
131
+ - `reallink logs consent --enable`
132
+ - Upload destination path in project storage:
133
+ - `.reallink/logs/cli/runtime/*`
134
+ - `.reallink/logs/cli/crash/*`
135
+
106
136
  ## Unreal Link Architecture
107
137
 
108
138
  `reallink link ...` provides a Vercel-style "link local project to remote project" workflow for local Unreal development.
package/bin/reallink.cjs CHANGED
@@ -34,8 +34,15 @@ function runBinary(binaryPath) {
34
34
  });
35
35
  }
36
36
 
37
+ function resolvePreferredBinary() {
38
+ if (fs.existsSync(prebuiltBinaryPath)) {
39
+ return prebuiltBinaryPath;
40
+ }
41
+ return resolveExistingReleaseBinary();
42
+ }
43
+
37
44
  function ensureBinary() {
38
- if (resolveExistingReleaseBinary() || fs.existsSync(prebuiltBinaryPath)) {
45
+ if (resolvePreferredBinary()) {
39
46
  return true;
40
47
  }
41
48
 
@@ -56,14 +63,14 @@ function ensureBinary() {
56
63
  return false;
57
64
  }
58
65
 
59
- return build.status === 0 && !!resolveExistingReleaseBinary();
66
+ return build.status === 0 && !!resolvePreferredBinary();
60
67
  }
61
68
 
62
69
  if (!ensureBinary()) {
63
70
  process.exit(1);
64
71
  }
65
72
 
66
- const binaryPath = resolveExistingReleaseBinary() || prebuiltBinaryPath;
73
+ const binaryPath = resolvePreferredBinary();
67
74
  const result = runBinary(binaryPath);
68
75
  if (result.error) {
69
76
  console.error(`Failed to run reallink binary: ${result.error.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallink-cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Rust-based CLI for Reallink auth and API operations",
5
5
  "bin": {
6
6
  "reallink": "bin/reallink.cjs"
Binary file
package/rust/Cargo.lock CHANGED
@@ -993,7 +993,7 @@ dependencies = [
993
993
 
994
994
  [[package]]
995
995
  name = "reallink-cli"
996
- version = "0.1.12"
996
+ version = "0.1.14"
997
997
  dependencies = [
998
998
  "anyhow",
999
999
  "clap",
package/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "reallink-cli"
3
- version = "0.1.12"
3
+ version = "0.1.14"
4
4
  edition = "2021"
5
5
  description = "CLI for Reallink auth and token workflows"
6
6
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
  // Do not edit by hand. Update packages/shared/src/api-contract.ts and re-run codegen.
3
3
  #![allow(dead_code)]
4
4
 
5
- pub const API_CONTRACT_VERSION: &str = "2026.03.0";
5
+ pub const API_CONTRACT_VERSION: &str = "2026.03.1";
6
6
  pub const API_TOKEN_SCOPES: &[&str] = &[
7
7
  "core:read",
8
8
  "core:write",
@@ -0,0 +1,277 @@
1
+ use anyhow::{Context, Result};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::collections::VecDeque;
4
+ use std::fs::{self, OpenOptions};
5
+ use std::io::Write;
6
+ use std::path::{Path, PathBuf};
7
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
+
9
+ const LOGS_DIR: &str = "logs";
10
+ const CRASH_DIR: &str = "crash";
11
+ const CONSENT_FILE: &str = "consent.json";
12
+ const RUNTIME_LOG_FILE: &str = "runtime.jsonl";
13
+
14
+ #[derive(Debug, Clone, Serialize, Deserialize)]
15
+ #[serde(rename_all = "camelCase")]
16
+ pub struct LogConsentConfig {
17
+ pub upload_enabled: bool,
18
+ pub asked_at_epoch_ms: Option<u128>,
19
+ pub updated_at_epoch_ms: u128,
20
+ }
21
+
22
+ impl Default for LogConsentConfig {
23
+ fn default() -> Self {
24
+ Self {
25
+ upload_enabled: false,
26
+ asked_at_epoch_ms: None,
27
+ updated_at_epoch_ms: now_epoch_ms(),
28
+ }
29
+ }
30
+ }
31
+
32
+ #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ #[serde(rename_all = "camelCase")]
34
+ pub struct RuntimeLogEvent {
35
+ pub ts_epoch_ms: u128,
36
+ pub level: String,
37
+ pub command: String,
38
+ pub message: String,
39
+ #[serde(skip_serializing_if = "Option::is_none")]
40
+ pub duration_ms: Option<u128>,
41
+ #[serde(skip_serializing_if = "Option::is_none")]
42
+ pub exit_code: Option<i32>,
43
+ }
44
+
45
+ #[derive(Debug, Clone, Serialize, Deserialize)]
46
+ #[serde(rename_all = "camelCase")]
47
+ pub struct CrashReport {
48
+ pub ts_epoch_ms: u128,
49
+ pub command: String,
50
+ pub message: String,
51
+ #[serde(skip_serializing_if = "Option::is_none")]
52
+ pub detail: Option<String>,
53
+ }
54
+
55
+ #[derive(Debug, Clone, Serialize)]
56
+ #[serde(rename_all = "camelCase")]
57
+ pub struct LogUploadCandidate {
58
+ pub local_path: PathBuf,
59
+ pub remote_path: String,
60
+ pub content_type: String,
61
+ }
62
+
63
+ #[derive(Debug, Clone, Serialize)]
64
+ #[serde(rename_all = "camelCase")]
65
+ pub struct LogStatus {
66
+ pub logs_root: String,
67
+ pub runtime_log_path: String,
68
+ pub crash_dir: String,
69
+ pub consent_file_path: String,
70
+ pub consent: LogConsentConfig,
71
+ pub runtime_exists: bool,
72
+ pub crash_report_count: usize,
73
+ }
74
+
75
+ fn now_epoch_ms() -> u128 {
76
+ SystemTime::now()
77
+ .duration_since(UNIX_EPOCH)
78
+ .unwrap_or_else(|_| Duration::from_secs(0))
79
+ .as_millis()
80
+ }
81
+
82
+ fn write_atomic(path: &Path, payload: &[u8]) -> Result<()> {
83
+ let temp_path = path.with_extension(format!("tmp.{}", now_epoch_ms()));
84
+ fs::write(&temp_path, payload)
85
+ .with_context(|| format!("Failed to write temporary file {}", temp_path.display()))?;
86
+ if path.exists() {
87
+ fs::remove_file(path).with_context(|| format!("Failed to replace {}", path.display()))?;
88
+ }
89
+ fs::rename(&temp_path, path).with_context(|| {
90
+ format!(
91
+ "Failed to move temporary file {} to {}",
92
+ temp_path.display(),
93
+ path.display()
94
+ )
95
+ })?;
96
+ Ok(())
97
+ }
98
+
99
+ fn logs_root(state_root: &Path) -> PathBuf {
100
+ state_root.join(LOGS_DIR)
101
+ }
102
+
103
+ fn crash_dir(state_root: &Path) -> PathBuf {
104
+ logs_root(state_root).join(CRASH_DIR)
105
+ }
106
+
107
+ fn consent_path(state_root: &Path) -> PathBuf {
108
+ logs_root(state_root).join(CONSENT_FILE)
109
+ }
110
+
111
+ fn runtime_log_path(state_root: &Path) -> PathBuf {
112
+ logs_root(state_root).join(RUNTIME_LOG_FILE)
113
+ }
114
+
115
+ fn ensure_logs_dirs(state_root: &Path) -> Result<()> {
116
+ fs::create_dir_all(logs_root(state_root))
117
+ .with_context(|| format!("Failed to create {}", logs_root(state_root).display()))?;
118
+ fs::create_dir_all(crash_dir(state_root))
119
+ .with_context(|| format!("Failed to create {}", crash_dir(state_root).display()))?;
120
+ Ok(())
121
+ }
122
+
123
+ pub fn load_consent(state_root: &Path) -> Result<LogConsentConfig> {
124
+ let path = consent_path(state_root);
125
+ if !path.exists() {
126
+ return Ok(LogConsentConfig::default());
127
+ }
128
+ let raw =
129
+ fs::read(&path).with_context(|| format!("Failed to read consent file {}", path.display()))?;
130
+ let parsed: LogConsentConfig = serde_json::from_slice(&raw)
131
+ .with_context(|| format!("Invalid consent file {}", path.display()))?;
132
+ Ok(parsed)
133
+ }
134
+
135
+ pub fn set_upload_consent(state_root: &Path, enabled: bool) -> Result<LogConsentConfig> {
136
+ ensure_logs_dirs(state_root)?;
137
+ let mut current = load_consent(state_root)?;
138
+ let now = now_epoch_ms();
139
+ current.upload_enabled = enabled;
140
+ current.updated_at_epoch_ms = now;
141
+ if current.asked_at_epoch_ms.is_none() {
142
+ current.asked_at_epoch_ms = Some(now);
143
+ }
144
+ save_consent(state_root, &current)?;
145
+ Ok(current)
146
+ }
147
+
148
+ pub fn save_consent(state_root: &Path, config: &LogConsentConfig) -> Result<()> {
149
+ ensure_logs_dirs(state_root)?;
150
+ let path = consent_path(state_root);
151
+ let payload = serde_json::to_vec_pretty(config)?;
152
+ write_atomic(&path, &payload)?;
153
+ Ok(())
154
+ }
155
+
156
+ pub fn append_runtime_event(state_root: &Path, event: &RuntimeLogEvent) -> Result<()> {
157
+ ensure_logs_dirs(state_root)?;
158
+ let path = runtime_log_path(state_root);
159
+ let mut writer = OpenOptions::new()
160
+ .create(true)
161
+ .append(true)
162
+ .open(&path)
163
+ .with_context(|| format!("Failed to open runtime log {}", path.display()))?;
164
+ let line = serde_json::to_string(event)?;
165
+ writer
166
+ .write_all(format!("{}\n", line).as_bytes())
167
+ .with_context(|| format!("Failed to write runtime log {}", path.display()))?;
168
+ Ok(())
169
+ }
170
+
171
+ pub fn write_crash_report(state_root: &Path, report: &CrashReport) -> Result<PathBuf> {
172
+ ensure_logs_dirs(state_root)?;
173
+ let file_name = format!("crash-{}.json", report.ts_epoch_ms);
174
+ let path = crash_dir(state_root).join(file_name);
175
+ let payload = serde_json::to_vec_pretty(report)?;
176
+ write_atomic(&path, &payload)?;
177
+ Ok(path)
178
+ }
179
+
180
+ fn file_name_string(path: &Path) -> Option<String> {
181
+ path.file_name()
182
+ .and_then(|value| value.to_str())
183
+ .map(|value| value.to_string())
184
+ }
185
+
186
+ fn content_type_for_path(path: &Path) -> String {
187
+ match path.extension().and_then(|value| value.to_str()) {
188
+ Some("json") => "application/json".to_string(),
189
+ Some("jsonl") => "application/x-ndjson".to_string(),
190
+ Some("log") | Some("txt") => "text/plain".to_string(),
191
+ _ => "application/octet-stream".to_string(),
192
+ }
193
+ }
194
+
195
+ pub fn list_upload_candidates(
196
+ state_root: &Path,
197
+ include_runtime: bool,
198
+ include_crash: bool,
199
+ ) -> Result<Vec<LogUploadCandidate>> {
200
+ ensure_logs_dirs(state_root)?;
201
+ let mut output = Vec::new();
202
+
203
+ if include_runtime {
204
+ let runtime_path = runtime_log_path(state_root);
205
+ if runtime_path.exists() && runtime_path.is_file() {
206
+ if let Some(file_name) = file_name_string(&runtime_path) {
207
+ output.push(LogUploadCandidate {
208
+ local_path: runtime_path.clone(),
209
+ remote_path: format!(".reallink/logs/cli/runtime/{}", file_name),
210
+ content_type: content_type_for_path(&runtime_path),
211
+ });
212
+ }
213
+ }
214
+ }
215
+
216
+ if include_crash {
217
+ let mut crash_files: Vec<PathBuf> = fs::read_dir(crash_dir(state_root))
218
+ .with_context(|| format!("Failed to read {}", crash_dir(state_root).display()))?
219
+ .filter_map(|entry| entry.ok().map(|item| item.path()))
220
+ .filter(|path| path.is_file())
221
+ .collect();
222
+ crash_files.sort();
223
+ for path in crash_files {
224
+ if let Some(file_name) = file_name_string(&path) {
225
+ output.push(LogUploadCandidate {
226
+ local_path: path.clone(),
227
+ remote_path: format!(".reallink/logs/cli/crash/{}", file_name),
228
+ content_type: content_type_for_path(&path),
229
+ });
230
+ }
231
+ }
232
+ }
233
+
234
+ Ok(output)
235
+ }
236
+
237
+ pub fn read_runtime_tail(state_root: &Path, lines: usize) -> Result<Vec<String>> {
238
+ let path = runtime_log_path(state_root);
239
+ if !path.exists() {
240
+ return Ok(Vec::new());
241
+ }
242
+ let content = fs::read_to_string(&path)
243
+ .with_context(|| format!("Failed to read runtime log {}", path.display()))?;
244
+ if lines == 0 {
245
+ return Ok(Vec::new());
246
+ }
247
+ let mut deque = VecDeque::with_capacity(lines);
248
+ for line in content.lines() {
249
+ if deque.len() == lines {
250
+ deque.pop_front();
251
+ }
252
+ deque.push_back(line.to_string());
253
+ }
254
+ Ok(deque.into_iter().collect())
255
+ }
256
+
257
+ pub fn status(state_root: &Path) -> Result<LogStatus> {
258
+ ensure_logs_dirs(state_root)?;
259
+ let runtime_path = runtime_log_path(state_root);
260
+ let crash_directory = crash_dir(state_root);
261
+ let crash_count = fs::read_dir(&crash_directory)
262
+ .with_context(|| format!("Failed to read crash directory {}", crash_directory.display()))?
263
+ .filter_map(|entry| entry.ok())
264
+ .filter(|entry| entry.path().is_file())
265
+ .count();
266
+ let consent = load_consent(state_root)?;
267
+
268
+ Ok(LogStatus {
269
+ logs_root: logs_root(state_root).display().to_string(),
270
+ runtime_log_path: runtime_path.display().to_string(),
271
+ crash_dir: crash_directory.display().to_string(),
272
+ consent_file_path: consent_path(state_root).display().to_string(),
273
+ consent,
274
+ runtime_exists: runtime_path.exists(),
275
+ crash_report_count: crash_count,
276
+ })
277
+ }