Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion crates/ov_cli/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use serde_json::Value;
use std::fs::File;
use std::path::Path;
use tempfile::{Builder, NamedTempFile};
use url::Url;
use zip::CompressionMethod;
use zip::write::FileOptions;

Expand Down Expand Up @@ -314,6 +313,34 @@ impl HttpClient {
self.get("/api/v1/content/overview", &params).await
}

pub async fn write(
&self,
uri: &str,
content: &str,
mode: &str,
wait: bool,
timeout: Option<f64>,
) -> Result<serde_json::Value> {
let body = Self::build_write_body(uri, content, mode, wait, timeout);
self.post("/api/v1/content/write", &body).await
}

fn build_write_body(
uri: &str,
content: &str,
mode: &str,
wait: bool,
timeout: Option<f64>,
) -> Value {
serde_json::json!({
"uri": uri,
"content": content,
"mode": mode,
"wait": wait,
"timeout": timeout,
})
}

pub async fn reindex(
&self,
uri: &str,
Expand Down Expand Up @@ -858,6 +885,7 @@ impl HttpClient {
#[cfg(test)]
mod tests {
use super::HttpClient;
use serde_json::json;

#[test]
fn build_headers_includes_tenant_identity_headers() {
Expand Down Expand Up @@ -897,4 +925,28 @@ mod tests {
Some("alice")
);
}

#[test]
fn build_write_body_omits_removed_semantic_flags() {
let body = HttpClient::build_write_body(
"viking://resources/demo.md",
"updated",
"replace",
true,
Some(3.0),
);

assert_eq!(
body,
json!({
"uri": "viking://resources/demo.md",
"content": "updated",
"mode": "replace",
"wait": true,
"timeout": 3.0,
})
);
assert!(body.get("regenerate_semantics").is_none());
assert!(body.get("revectorize").is_none());
}
}
23 changes: 23 additions & 0 deletions crates/ov_cli/src/commands/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,29 @@ pub async fn overview(
Ok(())
}

pub async fn write(
client: &HttpClient,
uri: &str,
content: &str,
append: bool,
wait: bool,
timeout: Option<f64>,
output_format: OutputFormat,
compact: bool,
) -> Result<()> {
let result = client
.write(
uri,
content,
if append { "append" } else { "replace" },
wait,
timeout,
)
.await?;
crate::output::output_success(result, output_format, compact);
Ok(())
}

pub async fn reindex(
client: &HttpClient,
uri: &str,
Expand Down
73 changes: 73 additions & 0 deletions crates/ov_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,26 @@ enum Commands {
/// Viking URI
uri: String,
},
/// Write text content to an existing file
Write {
/// Viking URI
uri: String,
/// Content to write
#[arg(long, conflicts_with = "from_file")]
content: Option<String>,
/// Read content from a local file
#[arg(long = "from-file", conflicts_with = "content")]
from_file: Option<String>,
/// Append instead of replacing the file
#[arg(long)]
append: bool,
/// Wait for async processing to finish
#[arg(long, default_value = "false")]
wait: bool,
/// Optional wait timeout in seconds
#[arg(long)]
timeout: Option<f64>,
},
/// Reindex content at URI (regenerates .abstract.md and .overview.md)
Reindex {
/// Viking URI
Expand Down Expand Up @@ -751,6 +771,15 @@ async fn main() {
Commands::Read { uri } => handle_read(uri, ctx).await,
Commands::Abstract { uri } => handle_abstract(uri, ctx).await,
Commands::Overview { uri } => handle_overview(uri, ctx).await,
Commands::Write {
uri,
content,
from_file,
append,
wait,
timeout,
} => handle_write(uri, content, from_file, append, wait, timeout, ctx)
.await,
Commands::Reindex {
uri,
regenerate,
Expand Down Expand Up @@ -1186,6 +1215,35 @@ async fn handle_overview(uri: String, ctx: CliContext) -> Result<()> {
commands::content::overview(&client, &uri, ctx.output_format, ctx.compact).await
}

async fn handle_write(
uri: String,
content: Option<String>,
from_file: Option<String>,
append: bool,
wait: bool,
timeout: Option<f64>,
ctx: CliContext,
) -> Result<()> {
let client = ctx.get_client();
let payload = match (content, from_file) {
(Some(value), None) => value,
(None, Some(path)) => std::fs::read_to_string(path)
.map_err(|e| Error::Client(format!("Failed to read --from-file: {}", e)))?,
_ => return Err(Error::Client("Specify exactly one of --content or --from-file".into())),
};
commands::content::write(
&client,
&uri,
&payload,
append,
wait,
timeout,
ctx.output_format,
ctx.compact,
)
.await
}

async fn handle_reindex(uri: String, regenerate: bool, wait: bool, ctx: CliContext) -> Result<()> {
let client = ctx.get_client();
commands::content::reindex(
Expand Down Expand Up @@ -1476,4 +1534,19 @@ mod tests {
assert_eq!(ctx.config.user.as_deref(), Some("from-cli-user"));
assert_eq!(ctx.config.agent_id.as_deref(), Some("from-cli-agent"));
}

#[test]
fn cli_write_rejects_removed_semantic_flags() {
let result = Cli::try_parse_from([
"ov",
"write",
"viking://resources/demo.md",
"--content",
"updated",
"--no-semantics",
"--no-vectorize",
]);

assert!(result.is_err(), "removed write flags should not parse");
}
}
1 change: 1 addition & 0 deletions docs/en/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ Compact JSON with status wrapper (when `--compact` is true, which is the default
| GET | `/api/v1/content/read` | Read full content (L2) |
| GET | `/api/v1/content/abstract` | Read abstract (L0) |
| GET | `/api/v1/content/overview` | Read overview (L1) |
| POST | `/api/v1/content/write` | Update an existing file and refresh semantics/vectors |

### Search

Expand Down
89 changes: 89 additions & 0 deletions docs/en/api/03-filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,95 @@ openviking read viking://resources/docs/api.md

---

### write()

Update an existing file and automatically refresh related semantics and vectors.

**Parameters**

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| uri | str | Yes | - | Existing file URI |
| content | str | Yes | - | New content to write |
| mode | str | No | `replace` | `replace` or `append` |
| wait | bool | No | `false` | Wait for background semantic/vector refresh |
| timeout | float | No | `null` | Timeout in seconds when `wait=true` |

**Notes**

- Only existing files are supported; directories are rejected.
- Derived semantic files cannot be written directly: `.abstract.md`, `.overview.md`, `.relations.json`.
- The public API no longer accepts `regenerate_semantics` or `revectorize`; write always refreshes related semantics and vectors.

**Python SDK (Embedded / HTTP)**

```python
result = client.write(
"viking://resources/docs/api.md",
"# Updated API\n\nFresh content.",
mode="replace",
wait=True,
)
print(result["root_uri"])
```

**HTTP API**

```
POST /api/v1/content/write
```

```bash
curl -X POST "http://localhost:1933/api/v1/content/write" \
-H "Content-Type: application/json" \
-H "X-API-Key: your-key" \
-d '{
"uri": "viking://resources/docs/api.md",
"content": "# Updated API\n\nFresh content.",
"mode": "replace",
"wait": true
}'
```

**CLI**

```bash
openviking write viking://resources/docs/api.md \
--content "# Updated API\n\nFresh content." \
--wait
```

**Response**

```json
{
"status": "ok",
"result": {
"uri": "viking://resources/docs/api.md",
"root_uri": "viking://resources/docs",
"context_type": "resource",
"mode": "replace",
"written_bytes": 29,
"semantic_updated": true,
"vector_updated": true,
"queue_status": {
"Semantic": {
"processed": 1,
"error_count": 0,
"errors": []
},
"Embedding": {
"processed": 2,
"error_count": 0,
"errors": []
}
}
}
}
```

---

### ls()

List directory contents.
Expand Down
1 change: 1 addition & 0 deletions docs/zh/api/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ openviking -o json ls viking://resources/
| GET | `/api/v1/content/read` | 读取完整内容(L2) |
| GET | `/api/v1/content/abstract` | 读取摘要(L0) |
| GET | `/api/v1/content/overview` | 读取概览(L1) |
| POST | `/api/v1/content/write` | 修改已有文件并自动刷新语义与向量 |

### 搜索

Expand Down
Loading
Loading