diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 091cfb1..f7014c3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.10.0" + ".": "0.11.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index c2cc39a..1ba5348 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 29 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-ecb277557634e2f0838ea196e2b21f11924f65e133c7977e8c2e1a603b8df466.yml -openapi_spec_hash: 798ce4cb042389a1179bf9af6fc1173d -config_hash: ff35e224e809656528c44163aa41bebd +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/context-dev/context.dev-c90f13e3cf4b9ae6a1d8f22c0e7285ab12d412d3a659a51a247a97adc7f08121.yml +openapi_spec_hash: bf7879be38ebf3397a939c80b261a47a +config_hash: daabb160675d86b354711da1e77e5129 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7546bd2..59f72b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.11.0 (2026-07-12) + +Full Changelog: [v0.10.0...v0.11.0](https://github.com/context-dot-dev/context-dev-cli/compare/v0.10.0...v0.11.0) + +### Features + +* **api:** api update ([0a8b58e](https://github.com/context-dot-dev/context-dev-cli/commit/0a8b58e4e26794d3c2a67f2d4ff92a85a8396d54)) +* **api:** api update ([16201c6](https://github.com/context-dot-dev/context-dev-cli/commit/16201c67e3dd1e192a0ca50557518d4bf8314580)) +* **api:** api update ([e636b6d](https://github.com/context-dot-dev/context-dev-cli/commit/e636b6d51d9c58cfb2f0845900e22eddcd939d50)) +* **api:** api update ([e16a2a9](https://github.com/context-dot-dev/context-dev-cli/commit/e16a2a9ef5a60f1b135bc081867e7404c1e2c8f5)) +* **api:** manual updates ([d742101](https://github.com/context-dot-dev/context-dev-cli/commit/d74210104ea1f0a693d91aa8f8b53d6ea433ec70)) + ## 0.10.0 (2026-07-10) Full Changelog: [v0.9.0...v0.10.0](https://github.com/context-dot-dev/context-dev-cli/compare/v0.9.0...v0.10.0) diff --git a/go.mod b/go.mod index f0d5d12..e43b180 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.1 - github.com/context-dot-dev/context-go-sdk/v2 v2.1.0 + github.com/context-dot-dev/context-go-sdk/v2 v2.2.0 github.com/goccy/go-yaml v1.18.0 github.com/itchyny/json2yaml v0.1.4 github.com/muesli/reflow v0.3.0 diff --git a/go.sum b/go.sum index 919a559..669bf87 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payR github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/context-dot-dev/context-go-sdk/v2 v2.1.0 h1:Npy3In0Wda7r+TPlvjnoErIe/6s4qZ80uymsC6QCrUQ= -github.com/context-dot-dev/context-go-sdk/v2 v2.1.0/go.mod h1:CvRANWLP0FHj5XyUwzEVX79ujdwr/2Jy375UVzKNQm8= +github.com/context-dot-dev/context-go-sdk/v2 v2.2.0 h1:4jIQ8PVy5Q27H/50je4gSrt5GwO4cpaRyy6idy2ZbCA= +github.com/context-dot-dev/context-go-sdk/v2 v2.2.0/go.mod h1:CvRANWLP0FHj5XyUwzEVX79ujdwr/2Jy375UVzKNQm8= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 85854aa..841a1e6 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -84,6 +84,14 @@ func init() { }, }, Commands: []*cli.Command{ + { + Name: "parse", + Category: "API RESOURCE", + Suggest: true, + Commands: []*cli.Command{ + &parseHandle, + }, + }, { Name: "web", Category: "API RESOURCE", diff --git a/pkg/cmd/monitor.go b/pkg/cmd/monitor.go index ffd0568..5729a84 100644 --- a/pkg/cmd/monitor.go +++ b/pkg/cmd/monitor.go @@ -80,9 +80,14 @@ var monitorsCreate = requestflag.WithInnerFlags(cli.Command{ "webhook": { &requestflag.InnerFlag[string]{ Name: "webhook.url", - Usage: "Webhook URL called when a change is detected.", + Usage: "Webhook URL events are delivered to.", InnerField: "url", }, + &requestflag.InnerFlag[[]string]{ + Name: "webhook.events", + Usage: "Events delivered to this endpoint. `change.detected` fires only when a run detects a change; `run.completed` fires on every completed run — including runs that detected no change — and embeds the change when one was detected. Defaults to `[\"change.detected\"]` when omitted.", + InnerField: "events", + }, &requestflag.InnerFlag[string]{ Name: "webhook.secret", Usage: "Signing secret used to verify webhook authenticity. Each delivery includes an `X-Context-Signature: t=,v1=` header, where the HMAC is SHA-256 over `\"{t}.{rawRequestBody}\"` keyed by this secret. Recompute it with a constant-time compare and reject stale timestamps to prevent replay. Generated by the API; cannot be set by clients.", @@ -174,9 +179,14 @@ var monitorsUpdate = requestflag.WithInnerFlags(cli.Command{ "webhook": { &requestflag.InnerFlag[string]{ Name: "webhook.url", - Usage: "Webhook URL called when a change is detected.", + Usage: "Webhook URL events are delivered to.", InnerField: "url", }, + &requestflag.InnerFlag[[]string]{ + Name: "webhook.events", + Usage: "Events delivered to this endpoint. `change.detected` fires only when a run detects a change; `run.completed` fires on every completed run — including runs that detected no change — and embeds the change when one was detected. Defaults to `[\"change.detected\"]` when omitted.", + InnerField: "events", + }, &requestflag.InnerFlag[string]{ Name: "webhook.secret", Usage: "Signing secret used to verify webhook authenticity. Each delivery includes an `X-Context-Signature: t=,v1=` header, where the HMAC is SHA-256 over `\"{t}.{rawRequestBody}\"` keyed by this secret. Recompute it with a constant-time compare and reject stale timestamps to prevent replay. Generated by the API; cannot be set by clients.", diff --git a/pkg/cmd/monitor_test.go b/pkg/cmd/monitor_test.go index 5745c6a..a307b8c 100644 --- a/pkg/cmd/monitor_test.go +++ b/pkg/cmd/monitor_test.go @@ -23,7 +23,7 @@ func TestMonitorsCreate(t *testing.T) { "--mode", "web", "--tag", "pricing", "--tag", "competitor", - "--webhook", "{url: https://example.com/webhook}", + "--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed]}", ) }) @@ -46,6 +46,7 @@ func TestMonitorsCreate(t *testing.T) { "--tag", "pricing", "--tag", "competitor", "--webhook.url", "https://example.com/webhook", + "--webhook.events", "[change.detected, run.completed]", ) }) @@ -68,7 +69,10 @@ func TestMonitorsCreate(t *testing.T) { " - pricing\n" + " - competitor\n" + "webhook:\n" + - " url: https://example.com/webhook\n") + " url: https://example.com/webhook\n" + + " events:\n" + + " - change.detected\n" + + " - run.completed\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--api-key", "string", @@ -104,7 +108,7 @@ func TestMonitorsUpdate(t *testing.T) { "--tag", "pricing", "--tag", "competitor", "--target", "{type: page, url: https://acme.com/pricing, normalize_whitespace: true}", - "--webhook", "{url: https://example.com/webhook}", + "--webhook", "{url: https://example.com/webhook, events: [change.detected, run.completed]}", ) }) @@ -128,6 +132,7 @@ func TestMonitorsUpdate(t *testing.T) { "--tag", "competitor", "--target", "{type: page, url: https://acme.com/pricing, normalize_whitespace: true}", "--webhook.url", "https://example.com/webhook", + "--webhook.events", "[change.detected, run.completed]", ) }) @@ -150,7 +155,10 @@ func TestMonitorsUpdate(t *testing.T) { " url: https://acme.com/pricing\n" + " normalize_whitespace: true\n" + "webhook:\n" + - " url: https://example.com/webhook\n") + " url: https://example.com/webhook\n" + + " events:\n" + + " - change.detected\n" + + " - run.completed\n") mocktest.TestRunMockTestWithPipeAndFlags( t, pipeData, "--api-key", "string", diff --git a/pkg/cmd/parse.go b/pkg/cmd/parse.go new file mode 100644 index 0000000..c34be84 --- /dev/null +++ b/pkg/cmd/parse.go @@ -0,0 +1,143 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/context-dot-dev/context-dev-cli/internal/apiquery" + "github.com/context-dot-dev/context-dev-cli/internal/binaryparam" + "github.com/context-dot-dev/context-dev-cli/internal/requestflag" + "github.com/context-dot-dev/context-go-sdk/v2" + "github.com/context-dot-dev/context-go-sdk/v2/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var parseHandle = cli.Command{ + Name: "handle", + Usage: "Converts raw text, source code, web/data, PDF, Microsoft Office, and image bytes\ninto LLM-usable Markdown.", + Suggest: true, + Flags: []cli.Flag{ + &requestflag.Flag[string]{ + Name: "body", + Required: true, + BodyRoot: true, + FileInput: true, + }, + &requestflag.Flag[string]{ + Name: "base-url", + Usage: "Optional HTTP(S) source document URL used to resolve relative links and image references. Relative references remain relative when omitted.", + QueryPath: "baseUrl", + }, + &requestflag.Flag[string]{ + Name: "extension", + Usage: "Optional file extension hint, such as pdf, docx, xlsx, pptx, html, json, csv, md, py, rtf, jpg, png, or txt.", + QueryPath: "extension", + }, + &requestflag.Flag[string]{ + Name: "filename", + Usage: "Optional filename hint used to infer the extension when extension is omitted.", + QueryPath: "filename", + }, + &requestflag.Flag[bool]{ + Name: "include-images", + Usage: "Include image references in Markdown output", + Default: false, + QueryPath: "includeImages", + }, + &requestflag.Flag[bool]{ + Name: "include-links", + Usage: "Preserve hyperlinks in Markdown output", + Default: true, + QueryPath: "includeLinks", + }, + &requestflag.Flag[bool]{ + Name: "ocr", + Usage: "When true for PDF inputs, detect and OCR images embedded in the selected pages, inserting recognized text at each image's position in page reading order while preserving the PDF text layer. pdfStart/pdfEnd limit the inclusive page range. This is separate from automatic scanned-PDF OCR fallback.", + Default: false, + QueryPath: "ocr", + }, + &requestflag.Flag[int64]{ + Name: "pdf-end", + Usage: "Last 1-based PDF page to parse. When omitted, parsing ends at the last page. Must be greater than or equal to pdfStart when both are provided.", + QueryPath: "pdfEnd", + }, + &requestflag.Flag[int64]{ + Name: "pdf-start", + Usage: "First 1-based PDF page to parse. When omitted, parsing starts at the first page.", + QueryPath: "pdfStart", + }, + &requestflag.Flag[bool]{ + Name: "shorten-base64-images", + Usage: "Shorten base64-encoded image data in the Markdown output", + Default: true, + QueryPath: "shortenBase64Images", + }, + &requestflag.Flag[bool]{ + Name: "use-main-content-only", + Usage: "Extract only the main content from HTML-like inputs", + Default: false, + QueryPath: "useMainContentOnly", + }, + }, + Action: handleParseHandle, + HideHelpCommand: true, +} + +func handleParseHandle(ctx context.Context, cmd *cli.Command) error { + client := contextdev.NewClient(getDefaultRequestOptions(cmd)...) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("body") && len(unusedArgs) > 0 { + cmd.Set("body", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + + bodyReader, stdinInUse, err := binaryparam.FileOrStdin(os.Stdin, cmd.Value("body").(string)) + if err != nil { + return fmt.Errorf("Failed on param '%s': %w", "body", err) + } + defer bodyReader.Close() + + options, err := flagOptions( + cmd, + apiquery.NestedQueryFormatBrackets, + apiquery.ArrayQueryFormatComma, + ApplicationOctetStream, + stdinInUse, + ) + if err != nil { + return err + } + + params := contextdev.ParseHandleParams{} + + var res []byte + options = append(options, option.WithResponseBodyInto(&res)) + _, err = client.Parse.Handle( + ctx, + bodyReader, + params, + options..., + ) + if err != nil { + return err + } + + obj := gjson.ParseBytes(res) + format := cmd.Root().String("format") + explicitFormat := cmd.Root().IsSet("format") + transform := cmd.Root().String("transform") + return ShowJSON(obj, ShowJSONOpts{ + ExplicitFormat: explicitFormat, + Format: format, + RawOutput: cmd.Root().Bool("raw-output"), + Title: "parse handle", + Transform: transform, + }) +} diff --git a/pkg/cmd/parse_test.go b/pkg/cmd/parse_test.go new file mode 100644 index 0000000..0612807 --- /dev/null +++ b/pkg/cmd/parse_test.go @@ -0,0 +1,51 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "testing" + + "github.com/context-dot-dev/context-dev-cli/internal/mocktest" +) + +func TestParseHandle(t *testing.T) { + t.Skip("Mock server tests are disabled") + t.Run("regular flags", func(t *testing.T) { + mocktest.TestRunMockTestWithFlags( + t, + "--api-key", "string", + "parse", "handle", + "--body", mocktest.TestFile(t, "Example data"), + "--base-url", "https://example.com", + "--extension", "extension", + "--filename", "filename", + "--include-images=true", + "--include-links=true", + "--ocr=true", + "--pdf-end", "1", + "--pdf-start", "1", + "--shorten-base64-images=true", + "--use-main-content-only=true", + ) + }) + + t.Run("piping data", func(t *testing.T) { + // Test piping YAML data over stdin + pipeData := []byte("Example data") + mocktest.TestRunMockTestWithPipeAndFlags( + t, pipeData, + "--api-key", "string", + "parse", "handle", + "--base-url", "https://example.com", + "--extension", "extension", + "--filename", "filename", + "--include-images=true", + "--include-links=true", + "--ocr=true", + "--pdf-end", "1", + "--pdf-start", "1", + "--shorten-base64-images=true", + "--use-main-content-only=true", + ) + }) +} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index e942183..c3995df 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.10.0" // x-release-please-version +const Version = "0.11.0" // x-release-please-version diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index 03a3e3e..4d16c84 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -475,8 +475,8 @@ var webWebCrawlMd = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[map[string]any]{ Name: "pdf", - Usage: "PDF parsing controls. Use start/end to limit text extraction and OCR to an inclusive 1-based page range.", - Default: map[string]any{"shouldParse": true}, + Usage: "PDF parsing controls. Use start/end to limit text extraction and embedded-image detection/OCR to an inclusive 1-based page range.", + Default: map[string]any{"shouldParse": true, "ocr": false}, BodyPath: "pdf", }, &requestflag.Flag[bool]{ @@ -528,6 +528,11 @@ var webWebCrawlMd = requestflag.WithInnerFlags(cli.Command{ Usage: "Last 1-based PDF page to parse. When omitted, parsing ends at the last page. Must be greater than or equal to start when both are provided.", InnerField: "end", }, + &requestflag.InnerFlag[bool]{ + Name: "pdf.ocr", + Usage: "When true, detect and OCR images embedded in the selected PDF pages, inserting recognized text at each image's position in page reading order while preserving the PDF text layer. This is separate from automatic scanned-PDF OCR fallback.", + InnerField: "ocr", + }, &requestflag.InnerFlag[bool]{ Name: "pdf.should-parse", Usage: "When true, PDF pages are fetched and parsed. When false, PDF pages are skipped entirely (not included in results and not counted as failures).", @@ -586,8 +591,8 @@ var webWebScrapeHTML = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[map[string]any]{ Name: "pdf", - Usage: "PDF parsing controls. Use start/end to limit text extraction and OCR to an inclusive 1-based page range.", - Default: map[string]any{"shouldParse": true}, + Usage: "PDF parsing controls. Use start/end to limit text extraction and embedded-image detection/OCR to an inclusive 1-based page range.", + Default: map[string]any{"shouldParse": true, "ocr": false}, QueryPath: "pdf", }, &requestflag.Flag[bool]{ @@ -622,6 +627,11 @@ var webWebScrapeHTML = requestflag.WithInnerFlags(cli.Command{ Usage: "Last 1-based PDF page to parse. When omitted, parsing ends at the last page. Must be greater than or equal to start when both are provided.", InnerField: "end", }, + &requestflag.InnerFlag[bool]{ + Name: "pdf.ocr", + Usage: "When true, detect and OCR images embedded in the selected PDF pages, inserting recognized text at each image's position in page reading order while preserving the PDF text layer. This is separate from automatic scanned-PDF OCR fallback.", + InnerField: "ocr", + }, &requestflag.InnerFlag[bool]{ Name: "pdf.should-parse", Usage: "When true, PDF URLs are fetched and parsed. When false, PDF URLs are skipped and a 400 WEBSITE_ACCESS_ERROR is returned.", @@ -708,7 +718,7 @@ var webWebScrapeImages = requestflag.WithInnerFlags(cli.Command{ var webWebScrapeMd = requestflag.WithInnerFlags(cli.Command{ Name: "web-scrape-md", - Usage: "Scrapes the given URL into LLM usable Markdown.", + Usage: "Scrapes the given URL into LLM usable Markdown. Inspect key_metadata on JSON\nresponses from a recognized API key; use error_code to distinguish stable\nfailure categories.", Suggest: true, Flags: []cli.Flag{ &requestflag.Flag[string]{ @@ -763,8 +773,8 @@ var webWebScrapeMd = requestflag.WithInnerFlags(cli.Command{ }, &requestflag.Flag[map[string]any]{ Name: "pdf", - Usage: "PDF parsing controls. Use start/end to limit text extraction and OCR to an inclusive 1-based page range.", - Default: map[string]any{"shouldParse": true}, + Usage: "PDF parsing controls. Use start/end to limit text extraction and embedded-image detection/OCR to an inclusive 1-based page range.", + Default: map[string]any{"shouldParse": true, "ocr": false}, QueryPath: "pdf", }, &requestflag.Flag[bool]{ @@ -805,6 +815,11 @@ var webWebScrapeMd = requestflag.WithInnerFlags(cli.Command{ Usage: "Last 1-based PDF page to parse. When omitted, parsing ends at the last page. Must be greater than or equal to start when both are provided.", InnerField: "end", }, + &requestflag.InnerFlag[bool]{ + Name: "pdf.ocr", + Usage: "When true, detect and OCR images embedded in the selected PDF pages, inserting recognized text at each image's position in page reading order while preserving the PDF text layer. This is separate from automatic scanned-PDF OCR fallback.", + InnerField: "ocr", + }, &requestflag.InnerFlag[bool]{ Name: "pdf.should-parse", Usage: "When true, PDF URLs are fetched and parsed. When false, PDF URLs are skipped and a 400 WEBSITE_ACCESS_ERROR is returned.", diff --git a/pkg/cmd/web_test.go b/pkg/cmd/web_test.go index 7280e80..ab8545d 100644 --- a/pkg/cmd/web_test.go +++ b/pkg/cmd/web_test.go @@ -285,7 +285,7 @@ func TestWebWebCrawlMd(t *testing.T) { "--max-age-ms", "0", "--max-depth", "0", "--max-pages", "1", - "--pdf", "{end: 1, shouldParse: true, start: 1}", + "--pdf", "{end: 1, ocr: true, shouldParse: true, start: 1}", "--settle-animations=true", "--shorten-base64-images=true", "--stop-after-ms", "10000", @@ -317,6 +317,7 @@ func TestWebWebCrawlMd(t *testing.T) { "--max-depth", "0", "--max-pages", "1", "--pdf.end", "1", + "--pdf.ocr=true", "--pdf.should-parse=true", "--pdf.start", "1", "--settle-animations=true", @@ -347,6 +348,7 @@ func TestWebWebCrawlMd(t *testing.T) { "maxPages: 1\n" + "pdf:\n" + " end: 1\n" + + " ocr: true\n" + " shouldParse: true\n" + " start: 1\n" + "settleAnimations: true\n" + @@ -378,7 +380,7 @@ func TestWebWebScrapeHTML(t *testing.T) { "--include-frames=true", "--include-selector", "string", "--max-age-ms", "0", - "--pdf", "{end: 1, shouldParse: true, start: 1}", + "--pdf", "{end: 1, ocr: true, shouldParse: true, start: 1}", "--settle-animations=true", "--timeout-ms", "1000", "--use-main-content-only=true", @@ -403,6 +405,7 @@ func TestWebWebScrapeHTML(t *testing.T) { "--include-selector", "string", "--max-age-ms", "0", "--pdf.end", "1", + "--pdf.ocr=true", "--pdf.should-parse=true", "--pdf.start", "1", "--settle-animations=true", @@ -469,7 +472,7 @@ func TestWebWebScrapeMd(t *testing.T) { "--include-links=true", "--include-selector", "string", "--max-age-ms", "0", - "--pdf", "{end: 1, shouldParse: true, start: 1}", + "--pdf", "{end: 1, ocr: true, shouldParse: true, start: 1}", "--settle-animations=true", "--shorten-base64-images=true", "--timeout-ms", "1000", @@ -497,6 +500,7 @@ func TestWebWebScrapeMd(t *testing.T) { "--include-selector", "string", "--max-age-ms", "0", "--pdf.end", "1", + "--pdf.ocr=true", "--pdf.should-parse=true", "--pdf.start", "1", "--settle-animations=true",