first commit
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# Raycast specific files
|
||||
raycast-env.d.ts
|
||||
.raycast-swift-build
|
||||
.swiftpm
|
||||
compiled_raycast_swift
|
||||
compiled_raycast_rust
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": false
|
||||
}
|
||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Thoughtful
|
||||
|
||||
A Raycast extension for quickly capturing ideas and opening your digital notebook.
|
||||
|
||||
## Features
|
||||
|
||||
- **Create Idea**: Quickly send ideas to your notebook via a configured API endpoint
|
||||
- **Open Thoughtful**: Open your Thoughtful app or notebook directly from Raycast
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
3. Run the extension in development mode:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
When you first run the "Create Idea" command, you'll be prompted to configure:
|
||||
|
||||
- **URL**: The API endpoint where ideas will be sent
|
||||
- **Link**: The URL to open when using "Open Thoughtful" command
|
||||
- **Cookie**: Authentication cookie for API requests
|
||||
- **Custom Headers** (optional): Up to two custom headers for API authentication
|
||||
|
||||
Configuration is stored in `~/.thoughtful-config.json`.
|
||||
|
||||
## Commands
|
||||
|
||||
### Create Idea
|
||||
|
||||
Creates a new idea in your notebook. The extension:
|
||||
1. Prompts you for configuration on first use
|
||||
2. Provides a form to enter your idea
|
||||
3. Sends the idea to your configured API endpoint
|
||||
4. Shows the response from your notebook
|
||||
|
||||
### Open Thoughtful
|
||||
|
||||
Opens your Thoughtful app or notebook in the default browser. Uses the link configured in the "Create Idea" command.
|
||||
|
||||
## Development
|
||||
|
||||
This extension is built with:
|
||||
- [Raycast API](https://developers.raycast.com/)
|
||||
- TypeScript
|
||||
- React
|
||||
|
||||
### Scripts
|
||||
|
||||
- `pnpm dev` - Run extension in development mode
|
||||
- `pnpm build` - Build the extension for production
|
||||
- `pnpm lint` - Lint the code
|
||||
- `pnpm fix-lint` - Fix linting issues
|
||||
- `pnpm publish` - Publish to Raycast Store
|
||||
|
||||
## Platform Support
|
||||
|
||||
- Windows
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
thoughtful
|
||||
BIN
assets/extension-icon.png
Normal file
BIN
assets/extension-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
6
eslint.config.js
Normal file
6
eslint.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const raycastConfig = require("@raycast/eslint-config");
|
||||
|
||||
module.exports = defineConfig([
|
||||
...raycastConfig,
|
||||
]);
|
||||
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"$schema": "https://www.raycast.com/schemas/extension.json",
|
||||
"name": "thoughtful",
|
||||
"title": "Thoughtful",
|
||||
"description": "Adds a idea to your your notebook",
|
||||
"icon": "extension-icon.png",
|
||||
"license": "MIT",
|
||||
"commands": [
|
||||
{
|
||||
"name": "create-thoughtful",
|
||||
"title": "Create Idea",
|
||||
"description": "Creates a new idea in your notebook",
|
||||
"mode": "view",
|
||||
"subtitle": "Creates a new idea in your notebook"
|
||||
},
|
||||
{
|
||||
"name": "open-thoughtful",
|
||||
"title": "Open Thoughtful",
|
||||
"description": "Open the Thoughtful app",
|
||||
"mode": "view",
|
||||
"subtitle": "Open the Thoughtful app"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.34.0",
|
||||
"@raycast/api": "^1.103.0",
|
||||
"@raycast/utils": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/eslint-config": "^2.0.4",
|
||||
"@types/node": "22.13.10",
|
||||
"@types/react": "19.0.10",
|
||||
"eslint": "^9.22.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "ray develop",
|
||||
"lint": "ray lint",
|
||||
"fix-lint": "ray lint --fix",
|
||||
"build": "ray build",
|
||||
"publish": "npx @raycast/api@latest publish",
|
||||
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1"
|
||||
},
|
||||
"author": "thoughtful",
|
||||
"platforms": [
|
||||
"Windows"
|
||||
],
|
||||
"categories": [
|
||||
"Applications"
|
||||
]
|
||||
}
|
||||
2249
pnpm-lock.yaml
generated
Normal file
2249
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
271
src/create-thoughtful.tsx
Normal file
271
src/create-thoughtful.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { Form, ActionPanel, Action, Detail, showToast, Toast } from "@raycast/api";
|
||||
import { useState, useEffect } from "react";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
interface Config {
|
||||
url: string;
|
||||
link?: string;
|
||||
cookie?: string;
|
||||
header1Name?: string;
|
||||
header1Value?: string;
|
||||
header2Name?: string;
|
||||
header2Value?: string;
|
||||
}
|
||||
|
||||
const CONFIG_FILE = path.join(os.homedir(), ".thoughtful-config.json");
|
||||
|
||||
function loadConfig(): Config | null {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading config:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveConfig(config: Config): void {
|
||||
try {
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
|
||||
} catch (error) {
|
||||
console.error("Error saving config:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Command() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [response, setResponse] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadedConfig = loadConfig();
|
||||
setConfig(loadedConfig);
|
||||
if (!loadedConfig || !loadedConfig.link) {
|
||||
setShowSetup(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function handleSetup(values: {
|
||||
url: string;
|
||||
link: string;
|
||||
cookie: string;
|
||||
header1Name: string;
|
||||
header1Value: string;
|
||||
header2Name: string;
|
||||
header2Value: string;
|
||||
}) {
|
||||
if (!values.url.trim()) {
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "URL is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(values.url);
|
||||
} catch {
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Invalid URL",
|
||||
message: "Please enter a valid URL",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig: Config = {
|
||||
url: values.url,
|
||||
link: values.link || undefined,
|
||||
cookie: values.cookie || undefined,
|
||||
header1Name: values.header1Name || undefined,
|
||||
header1Value: values.header1Value || undefined,
|
||||
header2Name: values.header2Name || undefined,
|
||||
header2Value: values.header2Value || undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
saveConfig(newConfig);
|
||||
setConfig(newConfig);
|
||||
setShowSetup(false);
|
||||
showToast({
|
||||
style: Toast.Style.Success,
|
||||
title: "Configuration saved",
|
||||
});
|
||||
} catch (error) {
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Error saving configuration",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(values: { title: string; description: string }) {
|
||||
if (!values.title.trim() || !values.description.trim()) {
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Please enter both title and description",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Configuration missing",
|
||||
});
|
||||
setShowSetup(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (config.cookie) {
|
||||
headers["Cookie"] = config.cookie;
|
||||
}
|
||||
|
||||
if (config.header1Name && config.header1Value) {
|
||||
headers[config.header1Name] = config.header1Value;
|
||||
}
|
||||
if (config.header2Name && config.header2Value) {
|
||||
headers[config.header2Name] = config.header2Value;
|
||||
}
|
||||
|
||||
const res = await fetch(config.url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ title: values.title, description: values.description }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const data = await res.text();
|
||||
const linkUrl = config.link || config.url;
|
||||
const responseWithLink = `${data}\n\n---\n\n[View Result](${linkUrl})`;
|
||||
setResponse(responseWithLink);
|
||||
showToast({
|
||||
style: Toast.Style.Success,
|
||||
title: "Response received",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error making request:", error);
|
||||
showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Error making request",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (showSetup || !config) {
|
||||
return (
|
||||
<Form
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Save Configuration" onSubmit={handleSetup} />
|
||||
{config && <Action title="Cancel" onAction={() => setShowSetup(false)} />}
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.TextField
|
||||
id="url"
|
||||
title="API URL"
|
||||
placeholder="https://api.example.com/endpoint"
|
||||
defaultValue={config?.url}
|
||||
autoFocus
|
||||
/>
|
||||
<Form.TextField
|
||||
id="link"
|
||||
title="Result Link URL"
|
||||
placeholder="https://example.com/view"
|
||||
defaultValue={config?.link}
|
||||
/>
|
||||
<Form.TextField
|
||||
id="cookie"
|
||||
title="Cookie Header"
|
||||
placeholder="session=value; another=value (optional)"
|
||||
defaultValue={config?.cookie}
|
||||
/>
|
||||
<Form.Separator />
|
||||
<Form.TextField
|
||||
id="header1Name"
|
||||
title="Header 1 Name"
|
||||
placeholder="Authorization (optional)"
|
||||
defaultValue={config?.header1Name}
|
||||
/>
|
||||
<Form.TextField
|
||||
id="header1Value"
|
||||
title="Header 1 Value"
|
||||
placeholder="Bearer token (optional)"
|
||||
defaultValue={config?.header1Value}
|
||||
/>
|
||||
<Form.Separator />
|
||||
<Form.TextField
|
||||
id="header2Name"
|
||||
title="Header 2 Name"
|
||||
placeholder="X-Custom-Header (optional)"
|
||||
defaultValue={config?.header2Name}
|
||||
/>
|
||||
<Form.TextField
|
||||
id="header2Value"
|
||||
title="Header 2 Value"
|
||||
placeholder="value (optional)"
|
||||
defaultValue={config?.header2Value}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
return (
|
||||
<Detail
|
||||
markdown=""
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action title="Submit Another Input" onAction={() => setResponse(null)} />
|
||||
<Action title="Change Configuration" onAction={() => setShowSetup(true)} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
isLoading={isLoading}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Submit" onSubmit={handleSubmit} />
|
||||
<Action title="Configure" onAction={() => setShowSetup(true)} />
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.TextField
|
||||
id="title"
|
||||
title="Title"
|
||||
placeholder="Enter title..."
|
||||
autoFocus
|
||||
/>
|
||||
<Form.TextArea
|
||||
id="description"
|
||||
title="Description"
|
||||
placeholder="Enter description..."
|
||||
enableMarkdown
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
72
src/open-thoughtful.tsx
Normal file
72
src/open-thoughtful.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { open, showToast, Toast, showHUD } from "@raycast/api";
|
||||
import { useEffect } from "react";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
|
||||
interface Config {
|
||||
url: string;
|
||||
link?: string;
|
||||
cookie?: string;
|
||||
header1Name?: string;
|
||||
header1Value?: string;
|
||||
header2Name?: string;
|
||||
header2Value?: string;
|
||||
}
|
||||
|
||||
const CONFIG_FILE = path.join(os.homedir(), ".thoughtful-config.json");
|
||||
|
||||
function loadConfig(): Config | null {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const data = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading config:", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Command() {
|
||||
useEffect(() => {
|
||||
async function openThoughtful() {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Configuration not found",
|
||||
message: "Please run 'Create Thoughtful' first to configure",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const linkUrl = config.link || config.url;
|
||||
|
||||
if (!linkUrl) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "No link configured",
|
||||
message: "Please configure a link in 'Create Thoughtful'",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await open(linkUrl, "com.google.Chrome"); // Keep as Chrome, this somehow opens the default app... (atleast on windows)
|
||||
await showHUD("Opening Thoughtful");
|
||||
} catch (error) {
|
||||
await showToast({
|
||||
style: Toast.Style.Failure,
|
||||
title: "Error opening link",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openThoughtful();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["src/**/*", "raycast-env.d.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2023"],
|
||||
"module": "commonjs",
|
||||
"target": "ES2023",
|
||||
"strict": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user