tokensave 7.9.0

Code intelligence tool that builds a semantic knowledge graph from Rust, Go, Java, Scala, TypeScript, Python, C, C++, Kotlin, C#, Swift, and many more codebases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
use std::path::Path;
use tokensave::tokensave::TokenSave;

/// Opens an existing project, or tells the user to run `tokensave init` first.
pub async fn ensure_initialized(project_path: &Path) -> tokensave::errors::Result<TokenSave> {
    if TokenSave::is_initialized(project_path) {
        return TokenSave::open(project_path).await;
    }
    Err(tokensave::errors::TokenSaveError::Config {
        message: format!(
            "no TokenSave index found at '{}' — run 'tokensave init' first",
            project_path.display()
        ),
    })
}

/// Fallback for `serve`: when CWD-based discovery fails, check the global DB
/// for registered projects. When multiple projects exist, pick the best match
/// against cwd: prefer a project that is an ancestor of cwd (cwd is inside the
/// project), then a project that is a descendant of cwd (project is under cwd).
/// Among multiple matches, the deepest (most specific) path wins.
pub async fn resolve_serve_from_global_db() -> Option<std::path::PathBuf> {
    let gdb = tokensave::global_db::GlobalDb::open().await?;
    let mut paths: Vec<String> = gdb.list_project_paths().await;
    // Keep only projects whose .tokensave dir still exists on disk.
    paths.retain(|p| {
        std::path::Path::new(p)
            .join(".tokensave/tokensave.db")
            .exists()
    });
    if paths.len() == 1 {
        return Some(std::path::PathBuf::from(paths.remove(0)));
    }
    if paths.is_empty() {
        return None;
    }

    // Multiple projects — try to resolve using cwd.
    let cwd = std::env::current_dir().ok()?;
    let cwd = cwd.canonicalize().unwrap_or(cwd);

    // Priority 1: cwd is inside a project (project is ancestor of cwd).
    // Pick the deepest ancestor (most specific match).
    let mut ancestors: Vec<_> = paths
        .iter()
        .filter_map(|p| {
            let pp = std::path::Path::new(p).canonicalize().ok()?;
            cwd.starts_with(&pp)
                .then(|| (pp.components().count(), p.clone()))
        })
        .collect();
    ancestors.sort_by_key(|a| std::cmp::Reverse(a.0)); // deepest first
    if let Some((_, best)) = ancestors.into_iter().next() {
        return Some(std::path::PathBuf::from(best));
    }

    // Priority 2: a project is under cwd (cwd is ancestor of project).
    // Pick the shallowest descendant (closest child).
    let mut descendants: Vec<_> = paths
        .iter()
        .filter_map(|p| {
            let pp = std::path::Path::new(p).canonicalize().ok()?;
            pp.starts_with(&cwd)
                .then(|| (pp.components().count(), p.clone()))
        })
        .collect();
    descendants.sort_by_key(|a| a.0); // shallowest first
    if let Some((_, best)) = descendants.into_iter().next() {
        return Some(std::path::PathBuf::from(best));
    }

    // No cwd-based match — report the ambiguity.
    eprintln!("Multiple tokensave projects found — pass -p <path> to select one:");
    for p in &paths {
        eprintln!("  {p}");
    }
    None
}

/// Last-resort fallback for `serve`: peek at the first stdin line to read the
/// MCP `initialize` request's `roots` array.  If a root matches a registered
/// project, return its path.  The raw line is stored in `out` so the caller
/// can replay it into the MCP transport (the server still needs to see it).
pub async fn resolve_serve_from_mcp_roots(out: &mut Option<String>) -> Option<std::path::PathBuf> {
    use tokio::io::AsyncBufReadExt;
    let stdin = tokio::io::stdin();
    let mut reader = tokio::io::BufReader::new(stdin);
    let mut line = String::new();
    // Read the first non-empty line (should be the `initialize` request).
    loop {
        line.clear();
        match reader.read_line(&mut line).await {
            Ok(0) => return None, // EOF
            Ok(_) => {
                let trimmed = line.trim();
                if !trimmed.is_empty() {
                    break;
                }
            }
            Err(_) => return None,
        }
    }
    *out = Some(line.trim().to_string());

    let parsed: serde_json::Value = serde_json::from_str(line.trim()).ok()?;
    let roots = parsed.pointer("/params/roots").and_then(|v| v.as_array())?;

    let gdb = tokensave::global_db::GlobalDb::open().await?;
    let registered: Vec<RegisteredProject> = gdb
        .list_project_paths()
        .await
        .into_iter()
        .filter_map(|p| {
            let pb = std::path::PathBuf::from(p);
            if pb.join(".tokensave/tokensave.db").exists() {
                let canonical = pb.canonicalize().ok();
                Some(RegisteredProject {
                    path: pb,
                    canonical,
                })
            } else {
                None
            }
        })
        .collect();

    // Try each root URI — first match wins.
    for root in roots {
        let uri = root.get("uri").and_then(|v| v.as_str()).unwrap_or_default();
        let Some(root_path) = root_uri_to_path(uri) else {
            tracing::debug!("skipping unsupported root URI: {}", uri);
            continue;
        };
        // Exact match: the root IS a registered project.
        if let Some(hit) = find_registered_project(&registered, &root_path) {
            tracing::info!("serving from MCP root: {}", hit.display());
            return Some(hit);
        }
        // Walk up from the root to find the nearest enclosing project.
        if let Some(discovered) = tokensave::config::discover_project_root(&root_path) {
            tracing::info!(
                "serving from discovered project under MCP root: {}",
                discovered.display()
            );
            return Some(discovered);
        }
    }
    None
}

/// A project registered in the global database.
struct RegisteredProject {
    path: std::path::PathBuf,
    canonical: Option<std::path::PathBuf>,
}

/// Finds the registered project that names the same directory as `root_path`.
///
/// Compares canonically as well as literally so that `D:\Dev\app` and
/// `D:/Dev/app` — the same directory spelled two ways — are recognized as one.
/// Canonicalization failure (e.g. the path does not exist) only disables the
/// canonical comparison; the literal one still applies. Returns the registered
/// spelling, not the root URI's, so downstream path comparisons keep working
/// against what the global DB recorded.
fn find_registered_project(
    registered: &[RegisteredProject],
    root_path: &std::path::Path,
) -> Option<std::path::PathBuf> {
    let canonical_root = root_path.canonicalize().ok();
    registered
        .iter()
        .find(|rp| {
            rp.path == root_path
                || match (rp.canonical.as_ref(), canonical_root.as_ref()) {
                    (Some(a), Some(b)) => a == b,
                    _ => false,
                }
        })
        .map(|rp| rp.path.clone())
}

/// Converts an MCP `roots` entry into a filesystem path.
///
/// Stripping a bare `file://` prefix is not enough. A client spells a Windows
/// root `file:///D:/Dev/app`, which leaves `/D:/Dev/app` — a path that matches
/// no registered project and that `discover_project_root` cannot walk, so the
/// whole roots-based fallback was dead on Windows. Percent-encoding was ignored
/// too, so any path containing a space (`file:///C:/My%20Project`) failed
/// everywhere. This is the inverse of `agents::kiro::file_resource_uri`, which
/// already produces exactly this form.
///
/// Values that are not URIs at all are returned as-is, since some clients send
/// a plain path.
fn root_uri_to_path(uri: &str) -> Option<std::path::PathBuf> {
    root_uri_to_path_on(uri, cfg!(windows))
}

/// The platform-independent half, so both behaviours stay testable wherever the
/// suite runs — CI is Linux, and gating the drive-letter rule on `cfg!(windows)`
/// alone would leave the Windows case, the one this exists for, uncovered.
///
/// `strip_drive_letter` belongs to the host, not the URI: `/C:/x` is a perfectly
/// legal POSIX path, so stripping it there would corrupt a real directory name.
fn root_uri_to_path_on(uri: &str, strip_drive_letter: bool) -> Option<std::path::PathBuf> {
    if uri.is_empty() {
        return None;
    }
    let path = match uri.strip_prefix("file://") {
        // `file://localhost/x` is the spelled-out form of `file:///x`, and
        // `file://127.0.0.1/x` / `file://[::1]/x` name the same local host.
        // The guard proved `rest`'s first `/`-delimited segment is one of
        // those three authorities, all pure ASCII, so slicing off exactly
        // that segment's byte length — not a hardcoded constant — cannot
        // land inside a multi-byte character and cannot go out of bounds.
        Some(rest) if authority_is_localhost(rest) => {
            let authority_len = rest.split('/').next().unwrap_or_default().len();
            percent_decode(&rest[authority_len..])?
        }
        // `file://host/share` uses a non-local authority. UNC roots are outside
        // this fallback's current scope, so do not reinterpret one as a relative path.
        Some(rest) if !rest.starts_with('/') => return None,
        Some(rest) => percent_decode(rest)?,
        None if uri.contains("://") => return None,
        None => uri.to_string(),
    };
    // `file://localhost` with no path decodes to "" — nothing to serve.
    if path.is_empty() {
        return None;
    }

    // `file:///D:/Dev/app` decodes to `/D:/Dev/app`; drop the leading slash that
    // belongs to the URI, not to the path, when a drive letter follows it.
    let path = match path.strip_prefix('/') {
        Some(rest) if strip_drive_letter && has_drive_prefix(rest) => rest.to_string(),
        _ => path,
    };

    Some(std::path::PathBuf::from(path))
}

/// True when the URI's authority component refers to the local machine.
///
/// Accepts `localhost`, `127.0.0.1`, and `[::1]` (host names are
/// case-insensitive).
///
/// Splits on `/` rather than slicing at a byte offset: this input comes
/// straight from an MCP client's `initialize` request, and a byte-indexed slice
/// panics when a multi-byte character straddles the cut (`localhosé/…` puts
/// byte 9 inside the `é`) — a malformed root must never take the server down.
fn authority_is_localhost(rest: &str) -> bool {
    rest.split('/').next().is_some_and(|authority| {
        authority.eq_ignore_ascii_case("localhost")
            || authority == "127.0.0.1"
            || authority == "[::1]"
    })
}

/// True when `path` starts with a Windows drive designator such as `C:`.
fn has_drive_prefix(path: &str) -> bool {
    let mut chars = path.chars();
    matches!((chars.next(), chars.next()), (Some(c), Some(':')) if c.is_ascii_alphabetic())
}

/// Decodes `%XX` escapes, leaving malformed sequences untouched. Returns
/// `None` when the decoded bytes are not valid UTF-8.
///
/// Decoding is done on bytes rather than chars so that a percent-encoded
/// multi-byte UTF-8 character reassembles correctly. An invalid decode must
/// reject the root rather than fall back to lossy replacement: `%C3`, `%FF`,
/// and every truncated multi-byte sequence would all collapse into the same
/// U+FFFD path — a directory the client never named, which a registered
/// project could then wrongly match.
fn percent_decode(input: &str) -> Option<String> {
    let bytes = input.as_bytes();
    if !bytes.contains(&b'%') {
        return Some(input.to_string());
    }
    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        if bytes[i] == b'%' && i + 2 < bytes.len() {
            if let (Some(hi), Some(lo)) = (
                (bytes[i + 1] as char).to_digit(16),
                (bytes[i + 2] as char).to_digit(16),
            ) {
                out.push((hi * 16 + lo) as u8);
                i += 3;
                continue;
            }
        }
        out.push(bytes[i]);
        i += 1;
    }
    String::from_utf8(out).ok()
}

#[cfg(test)]
mod tests {
    use super::{
        find_registered_project, has_drive_prefix, percent_decode, root_uri_to_path,
        root_uri_to_path_on,
    };
    use std::path::PathBuf;

    /// Resolves as a Windows host would.
    fn win(uri: &str) -> String {
        render(root_uri_to_path_on(uri, true))
    }

    /// Resolves as a Unix host would.
    fn unix(uri: &str) -> String {
        render(root_uri_to_path_on(uri, false))
    }

    fn render(path: Option<PathBuf>) -> String {
        path.unwrap_or_else(|| PathBuf::from("<none>"))
            .to_string_lossy()
            .replace('\\', "/")
    }

    #[test]
    fn windows_root_drops_the_uri_leading_slash() {
        assert_eq!(win("file:///D:/Dev/app"), "D:/Dev/app");
        assert_eq!(win("file:///c:/dev/app"), "c:/dev/app");
    }

    #[test]
    fn a_drive_letter_is_only_a_drive_letter_on_windows() {
        // `/C:/x` is a legal POSIX path; mangling it into a relative `C:/x`
        // would point at a different directory entirely.
        assert_eq!(unix("file:///C:/x"), "/C:/x");
    }

    #[test]
    fn windows_keeps_the_leading_slash_without_a_drive_prefix() {
        assert_eq!(win("file:///home/user/app"), "/home/user/app");
        // A single-letter first component is not a drive letter.
        assert_eq!(win("file:///d/dev/app"), "/d/dev/app");
    }

    #[test]
    fn localhost_authority_is_the_local_machine() {
        // `file://localhost/x` is the spelled-out form of `file:///x`, unlike a
        // real host, which would be a UNC share. Host names are
        // case-insensitive, so any casing counts.
        assert_eq!(unix("file://localhost/home/user/app"), "/home/user/app");
        assert_eq!(win("file://localhost/D:/Dev/app"), "D:/Dev/app");
        assert_eq!(unix("file://LOCALHOST/home/user/app"), "/home/user/app");
        assert_eq!(unix("file://LocalHost/home/user/app"), "/home/user/app");
        // A host that merely *starts* with the word is a real (UNC) host.
        assert!(root_uri_to_path("file://localhostname/share").is_none());
    }

    #[test]
    fn ip_literal_authorities_are_the_local_machine() {
        // `127.0.0.1` and `[::1]` are as long, respectively longer/shorter
        // than `localhost` — the authority-slicing must key off the actual
        // matched authority, not a hardcoded length, or these either
        // misparse or panic on a short path.
        assert_eq!(unix("file://127.0.0.1/home/user/app"), "/home/user/app");
        assert_eq!(win("file://127.0.0.1/D:/Dev/app"), "D:/Dev/app");
        assert_eq!(unix("file://[::1]/home/user/app"), "/home/user/app");
        assert_eq!(win("file://[::1]/D:/Dev/app"), "D:/Dev/app");
        // Regression: `[::1]` is shorter than `localhost`, so a URI whose
        // path is short enough previously indexed out of bounds and panicked.
        assert_eq!(unix("file://[::1]/x"), "/x");
        // A host that merely resembles a local authority is still a real host.
        assert!(root_uri_to_path("file://127.0.0.1.evil.com/x").is_none());
    }

    #[test]
    fn a_multibyte_authority_is_rejected_without_panicking() {
        // This input arrives from an MCP client's `initialize` request. A
        // byte-indexed authority check panicked here: `localhosé` puts byte 9
        // inside the two-byte `é`, and slicing off a char boundary aborts —
        // letting a malformed root take the whole server down.
        assert!(root_uri_to_path("file://localhosé/share").is_none());
        assert!(root_uri_to_path("file://locälhost/x").is_none());
        assert!(root_uri_to_path("file://é/x").is_none());
    }

    #[test]
    fn percent_escapes_are_decoded() {
        assert_eq!(win("file:///C:/My%20Project"), "C:/My Project");
        assert_eq!(win("file:///home/user/my%20app"), "/home/user/my app");
        assert_eq!(win("file:///home/user/caf%C3%A9"), "/home/user/café");
    }

    #[test]
    fn malformed_escapes_are_left_alone() {
        assert_eq!(percent_decode("100%"), Some("100%".to_string()));
        assert_eq!(percent_decode("a%zz"), Some("a%zz".to_string()));
        assert_eq!(percent_decode("a%4"), Some("a%4".to_string()));
        assert_eq!(percent_decode("%41"), Some("A".to_string()));
    }

    #[test]
    fn escapes_decoding_to_invalid_utf8_reject_the_root() {
        // `%C3` is a lone UTF-8 lead byte, `%FF` a byte no UTF-8 sequence
        // allows, `%E2%82` a truncated three-byte sequence. Lossy replacement
        // would collapse all three into the same `…/�` path — a directory the
        // client never named — so the root is rejected instead.
        assert!(root_uri_to_path("file:///tmp/%C3").is_none());
        assert!(root_uri_to_path("file:///tmp/%FF").is_none());
        assert!(root_uri_to_path("file:///tmp/%E2%82").is_none());
        assert!(root_uri_to_path("file://localhost/%FF").is_none());
        // A complete multi-byte sequence still decodes.
        assert_eq!(percent_decode("caf%C3%A9"), Some("café".to_string()));
    }

    #[test]
    fn a_localhost_authority_without_a_path_is_rejected() {
        // `file://localhost` leaves an empty path once the authority is
        // sliced off — nothing to serve.
        assert!(root_uri_to_path("file://localhost").is_none());
        assert!(root_uri_to_path("file://[::1]").is_none());
    }

    #[test]
    fn bare_paths_pass_through() {
        assert_eq!(win("/home/user/app"), "/home/user/app");
        assert_eq!(win("D:/Dev/app"), "D:/Dev/app");
        // The host-dependent entry point resolves for the platform it targets.
        assert!(root_uri_to_path("/home/user/app").is_some());
    }

    #[test]
    fn unsupported_uris_are_skipped() {
        assert!(root_uri_to_path("").is_none());
        assert!(root_uri_to_path("https://example.com/app").is_none());
        // UNC authorities are intentionally outside this fallback's current scope.
        assert!(root_uri_to_path("file://server/share").is_none());
    }

    #[test]
    fn drive_prefix_detection() {
        assert!(has_drive_prefix("C:/x"));
        assert!(has_drive_prefix("z:"));
        assert!(!has_drive_prefix("/C:/x"));
        assert!(!has_drive_prefix("home/user"));
        assert!(!has_drive_prefix("1:/x"));
        assert!(!has_drive_prefix(""));
    }

    #[test]
    fn registered_match_literal_even_when_the_path_does_not_exist() {
        // Canonicalization fails on a nonexistent path; the literal comparison
        // must still hold on its own.
        let tmp = tempfile::TempDir::new().unwrap();
        let missing = tmp.path().join("missing");
        assert!(!missing.exists());

        let registered = vec![missing.to_string_lossy().into_owned()];
        assert_eq!(
            find_registered_project(&prep(registered), &missing),
            Some(missing)
        );
    }

    #[test]
    fn registered_match_via_canonicalization_returns_the_registered_spelling() {
        // `project/../project` names the same existing directory as `project`
        // without stepping outside the temp dir; only canonicalization can
        // equate the two spellings.
        let tmp = tempfile::TempDir::new().unwrap();
        let project = tmp.path().join("project");
        std::fs::create_dir(&project).unwrap();
        let registered = vec![project.to_string_lossy().into_owned()];
        let alias = project.join("..").join("project");
        assert_ne!(alias.as_path(), project.as_path());
        assert_eq!(
            find_registered_project(&prep(registered), &alias),
            Some(project.clone())
        );
    }

    fn prep(registered: Vec<String>) -> Vec<super::RegisteredProject> {
        registered
            .into_iter()
            .map(|p| {
                let pb = PathBuf::from(p);
                let canonical = pb.canonicalize().ok();
                super::RegisteredProject {
                    path: pb,
                    canonical,
                }
            })
            .collect()
    }

    #[test]
    fn registered_mismatch_between_two_existing_directories() {
        let tmp = tempfile::TempDir::new().unwrap();
        let a = tmp.path().join("a");
        let b = tmp.path().join("b");
        std::fs::create_dir(&a).unwrap();
        std::fs::create_dir(&b).unwrap();
        let registered = vec![a.to_string_lossy().into_owned()];
        assert_eq!(find_registered_project(&prep(registered), &b), None);
    }
}