slingit 0.1.0 → 0.1.1

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
@@ -12,7 +12,13 @@ npx slingit shot analysis.md
12
12
  cat analysis.md | npx slingit shot
13
13
 
14
14
  # Send the link by email (the email contains the full working link)
15
- npx slingit shot analysis.md --tomail someone@company.com
15
+ npx slingit shot analysis.md --mailto someone@company.com
16
+
17
+ # Add a short message visible only in the email
18
+ npx slingit shot analysis.md --mailto someone@company.com --mailmessage "Context before you open this"
19
+
20
+ # Add a sender signature to the note page and email
21
+ npx slingit shot analysis.md --from "Marcin"
16
22
 
17
23
  # Public note: no encryption, server-side render, SEO
18
24
  npx slingit shot analysis.md --public
@@ -22,6 +28,9 @@ npx slingit shot analysis.md --expire 7
22
28
 
23
29
  # Skill for Claude Code ("send these findings to X's email")
24
30
  npx slingit install-skill
31
+
32
+ # Skill for Codex
33
+ npx slingit install-skill --target codex
25
34
  ```
26
35
 
27
36
  ## Configuration
@@ -42,17 +51,31 @@ Env takes precedence over the file; the file takes precedence over the built-in
42
51
  | Flag | Description | Default |
43
52
  |---|---|---|
44
53
  | `--public` | Disables encryption, enables server-side render (SEO/preview) | off (encrypted) |
45
- | `--tomail [email]` | Send the link by email; without a value, uses `defaultToMail` | off |
54
+ | `--mailto [email]` | Send the link by email; without a value, uses `defaultToMail` | off |
55
+ | `--mailmessage <text>` | Add an optional message/comment to the email body; requires `--mailto` | off |
46
56
  | `--expire <days>` | Delete the note after X days (1-365) | no TTL |
47
57
  | `--title <text>` | Title (with `--public`, used for preview/SEO) | first `# H1` |
58
+ | `--from <text>` | Optional sender/signature shown on the note page and email | hidden |
48
59
  | `--copy` | Copy the link to the clipboard | off |
49
60
 
61
+ ## Skill installation
62
+
63
+ ```bash
64
+ npx slingit install-skill # Claude Code, user profile
65
+ npx slingit install-skill --project # Claude Code, current repo
66
+ npx slingit install-skill --target codex # Codex, user profile
67
+ npx slingit install-skill --target all # Claude Code + Codex
68
+ ```
69
+
70
+ Claude Code installs to `~/.claude/skills/slingit/` or `./.claude/skills/slingit/`.
71
+ Codex installs to `~/.agents/skills/slingit/` or `./.agents/skills/slingit/`.
72
+
50
73
  ## Privacy model
51
74
 
52
75
  - **Default (encrypted):** content is encrypted with AES-256-GCM locally; only the
53
76
  ciphertext goes to the server. The key in the `#` URL fragment is never sent by the
54
77
  browser — full zero-knowledge.
55
- - **`--tomail` + encrypted:** the key passes through the server (in transit, not stored)
78
+ - **`--mailto` + encrypted:** the key passes through the server (in transit, not stored)
56
79
  and the mail — a deliberate decision: since the recipient needs the key anyway, they
57
80
  get the full link. Security model: capability URL — whoever has the link can read it.
58
81
  - **`--public`:** a deliberate opt-out from encryption (plaintext, SEO, nice preview).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slingit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Fire-and-forget: share formatted Markdown notes via link. Encrypted by default (key in #URL), optional email delivery.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,4 +26,4 @@
26
26
  ],
27
27
  "author": "BTA Systems",
28
28
  "license": "MIT"
29
- }
29
+ }
package/skill/SKILL.md CHANGED
@@ -9,49 +9,104 @@ You publish formatted Markdown notes with `npx slingit shot`. Each note gets a s
9
9
  link `https://slingit.dev/{id}`; by default it is **end-to-end encrypted** — the AES-256
10
10
  key lives only in the `#` fragment of the URL, so the server cannot read the content.
11
11
 
12
- ## Steps
13
-
14
- 1. Collect the session's findings into a `.md` file (in a temp/scratchpad directory).
15
- Format it properly: `##` headings, code blocks with a language tag (```php, ```ts etc.),
16
- first line as `# Title`.
17
- 2. Run the CLI (works out of the box no token or configuration required):
18
- ```bash
19
- npx slingit shot <file.md> [flags]
20
- ```
21
- 3. Return the full link from stdout to the user in chat (for an encrypted note the link
22
- contains the key after `#` — without it the content cannot be decrypted).
12
+ ## Choose the input method first
13
+
14
+ Use the method that matches the user's request:
15
+
16
+ 1. **Conversation/session content**: if the user asks to send findings, a summary, details
17
+ from this conversation, or anything you are about to compose, pipe Markdown directly to
18
+ `npx slingit shot`. Do **not** create a temporary `.md` file just to pass generated
19
+ conversation content.
20
+ 2. **Existing Markdown file**: if the user gives a specific `.md` file or asks to publish
21
+ a file from the workspace, pass that file path to `npx slingit shot <file.md>`.
22
+
23
+ Always format the Markdown properly: first line as `# Title`, `##` headings, and code
24
+ blocks with language tags (```php, ```ts, ```bash, etc.).
25
+
26
+ ## Commands
27
+
28
+ For generated conversation content, use stdin/pipe:
29
+
30
+ ```bash
31
+ cat <<'EOF' | npx slingit shot [flags]
32
+ # Title
33
+
34
+ Markdown content from the conversation.
35
+ EOF
36
+ ```
37
+
38
+ In PowerShell, use a here-string:
39
+
40
+ ```powershell
41
+ @'
42
+ # Title
43
+
44
+ Markdown content from the conversation.
45
+ '@ | npx slingit shot [flags]
46
+ ```
47
+
48
+ For an existing Markdown file, use the file path:
49
+
50
+ ```bash
51
+ npx slingit shot <file.md> [flags]
52
+ ```
53
+
54
+ Return the full link from stdout to the user in chat. For an encrypted note the link
55
+ contains the key after `#`; without it the content cannot be decrypted.
23
56
 
24
57
  ## Choosing flags based on the user's intent
25
58
 
26
59
  | Intent | Flags |
27
60
  |---|---|
28
- | "send it to X's email" | `--tomail X` (the email contains the full working link) |
29
- | "maximum privacy" | **no** `--tomail` — the default; the link with the key stays in chat only, the user shares it themselves |
61
+ | "send it to X's email" | `--mailto X` (the email contains the full working link) |
62
+ | "include this message/comment in the email" | `--mailmessage "..."` (only together with `--mailto`) |
63
+ | "maximum privacy" | **no** `--mailto` — the default; the link with the key stays in chat only, the user shares it themselves |
30
64
  | "public link" (for a PR, docs, SEO) | `--public` (no encryption, server-side render) |
31
65
  | "make it disappear after X days" | `--expire X` |
66
+ | "send from X", "signed by X", "from me/Marcin/team name" | `--from "X"` |
32
67
  | title different from the H1 | `--title "..."` |
33
68
 
34
- Flags can be combined, e.g. `--public --tomail x@y.z --expire 7`.
69
+ Flags can be combined, e.g. `--public --mailto x@y.z --mailmessage "Read the summary first" --from "Marcin" --expire 7`.
35
70
 
36
71
  ## Rules (do not break these)
37
72
 
38
73
  - Encryption is the **default** — there is no `--encrypt` flag; `--public` is a deliberate
39
74
  opt-out (use it only when the user explicitly wants a public/indexable note).
40
- - The encryption key must **never** end up in files, logs, or storage. With `--tomail`,
75
+ - The encryption key must **never** end up in files, logs, or storage. With `--mailto`,
41
76
  the CLI passes it to the server only in the `mailFragment` field, used solely to compose
42
77
  the link inside the email (transit only, never stored). Do not print the key separately —
43
78
  only as part of the full link.
44
- - When the user asks for "maximum privacy", skip `--tomail` even if you know the address —
79
+ - When the user asks for "maximum privacy", skip `--mailto` even if you know the address —
45
80
  zero-knowledge only holds when the key never leaves the terminal/chat.
46
81
  - No token is required — the CLI ships with a built-in one. `SLINGIT_TOKEN` (env) or
47
82
  `"token"` in `~/.slingit/config.json` are optional overrides (e.g. a self-hosted
48
83
  instance) — never invent a token.
84
+ - Use `--from` only when the user explicitly gives a sender/signature. If it is not
85
+ provided, do not invent one.
86
+ - Use `--mailmessage` only when the user explicitly asks to add a message/comment to
87
+ the email. It requires `--mailto` and is not part of the note page.
49
88
 
50
89
  ## Example
51
90
 
91
+ Conversation content:
92
+
52
93
  ```
53
- > /slingit boss@company.com
54
- 1. Save the findings to analysis.md (with a # Title and language-tagged code blocks)
55
- 2. npx slingit shot analysis.md --tomail boss@company.com
94
+ User asks: "Send these findings to boss@company.com from Marcin."
95
+ 1. Compose Markdown directly from the conversation.
96
+ 2. Pipe it to:
97
+ cat <<'EOF' | npx slingit shot --mailto boss@company.com --mailmessage "Please read the recommendation first." --from "Marcin"
98
+ # Title
99
+
100
+ ...
101
+ EOF
56
102
  3. Reply: "Sent. Link (contains the decryption key): https://slingit.dev/aX7k2mQp9f#Hk3..."
57
103
  ```
104
+
105
+ Existing file:
106
+
107
+ ```
108
+ User asks: "Send docs/review.md to boss@company.com."
109
+ 1. Use the file directly:
110
+ npx slingit shot docs/review.md --mailto boss@company.com
111
+ 2. Reply with the full link from stdout.
112
+ ```
package/src/cli.js CHANGED
@@ -7,17 +7,24 @@ const HELP = `slingit — share Markdown notes via link (encrypted by default)
7
7
 
8
8
  Usage:
9
9
  slingit shot [file.md] [flags] publish a note (file or stdin)
10
- slingit install-skill [--project] install the Claude Code skill
10
+ slingit install-skill [flags] install the agent skill
11
11
  slingit help this help
12
12
 
13
13
  Flags (shot):
14
14
  --public disable encryption: plaintext, server-side render, SEO
15
- --tomail [email] send the link by email (Resend); if omitted, uses
15
+ --mailto [email] send the link by email (Resend); if omitted, uses
16
16
  defaultToMail from ~/.slingit/config.json
17
+ --mailmessage <text>
18
+ optional message included in the email only; requires --mailto
17
19
  --expire <days> delete the note after X days (1-365)
18
20
  --title <text> note title (with --public, used for preview/SEO)
21
+ --from <text> optional sender/signature shown on the note page and email
19
22
  --copy copy the link to the clipboard
20
23
 
24
+ Flags (install-skill):
25
+ --target <name> claude (default), codex, or all
26
+ --project install into this repository instead of your user profile
27
+
21
28
  Configuration (optional — the CLI works out of the box, no config needed):
22
29
  SLINGIT_TOKEN override the built-in bearer token (e.g. self-hosted instance)
23
30
  SLINGIT_URL server address (defaults to the official SlingIt instance)
@@ -3,22 +3,53 @@ import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- /**
7
- * Copies SKILL.md to ~/.claude/skills/slingit/ (default)
8
- * or ./.claude/skills/slingit/ with --project.
9
- */
10
6
  export async function installSkill(args) {
11
- const project = args.includes('--project');
7
+ const opts = parseArgs(args);
12
8
  const source = fileURLToPath(new URL('../skill/SKILL.md', import.meta.url));
13
- const targetDir = project
14
- ? join(process.cwd(), '.claude', 'skills', 'slingit')
15
- : join(homedir(), '.claude', 'skills', 'slingit');
9
+ const targets = opts.target === 'all' ? ['claude', 'codex'] : [opts.target];
16
10
 
17
- await mkdir(targetDir, { recursive: true });
18
- const target = join(targetDir, 'SKILL.md');
19
- await copyFile(source, target);
11
+ for (const targetName of targets) {
12
+ const targetDir = targetPath(targetName, opts.project);
13
+ await mkdir(targetDir, { recursive: true });
14
+ const target = join(targetDir, 'SKILL.md');
15
+ await copyFile(source, target);
16
+ console.log(`✔ Skill installed (${targetName}): ${target}`);
17
+ }
20
18
 
21
- console.log(`✔ Skill installed: ${target}`);
22
- console.log(' In a Claude Code session, ask e.g.: "send these findings to X\'s email" or "/slingit".');
19
+ console.log(' Ask e.g.: "send these findings to X\'s email" or invoke the slingit skill directly.');
23
20
  console.log(' Works out of the box — no token or configuration needed.');
24
21
  }
22
+
23
+ function parseArgs(args) {
24
+ const opts = { project: false, target: 'claude' };
25
+ for (let i = 0; i < args.length; i++) {
26
+ const a = args[i];
27
+ switch (a) {
28
+ case '--project':
29
+ opts.project = true;
30
+ break;
31
+ case '--target': {
32
+ const target = args[++i];
33
+ if (!['claude', 'codex', 'all'].includes(target)) {
34
+ throw new Error('--target must be one of: claude, codex, all');
35
+ }
36
+ opts.target = target;
37
+ break;
38
+ }
39
+ default:
40
+ throw new Error(`unknown flag "${a}". See: slingit help`);
41
+ }
42
+ }
43
+ return opts;
44
+ }
45
+
46
+ function targetPath(target, project) {
47
+ if (target === 'claude') {
48
+ return project
49
+ ? join(process.cwd(), '.claude', 'skills', 'slingit')
50
+ : join(homedir(), '.claude', 'skills', 'slingit');
51
+ }
52
+ return project
53
+ ? join(process.cwd(), '.agents', 'skills', 'slingit')
54
+ : join(homedir(), '.agents', 'skills', 'slingit');
55
+ }
package/src/shot.js CHANGED
@@ -10,23 +10,29 @@ export async function shot(args) {
10
10
  const config = await loadConfig();
11
11
 
12
12
  let sentTo = null;
13
- if (opts.tomail !== undefined) {
14
- sentTo = opts.tomail || config.defaultToMail;
13
+ if (opts.mailto !== undefined) {
14
+ sentTo = opts.mailto || config.defaultToMail;
15
15
  if (!sentTo) {
16
- throw new Error('--tomail given without an address and no defaultToMail in ~/.slingit/config.json');
16
+ throw new Error('--mailto given without an address and no defaultToMail in ~/.slingit/config.json');
17
17
  }
18
18
  if (!EMAIL_RE.test(sentTo)) throw new Error(`invalid email address: ${sentTo}`);
19
19
  }
20
+ if (opts.mailmessage && !sentTo) {
21
+ throw new Error('--mailmessage can only be used together with --mailto');
22
+ }
20
23
 
21
24
  const markdown = await readInput(opts.file);
22
25
  if (!markdown.trim()) throw new Error('empty note — nothing to publish');
23
26
 
24
27
  const title = opts.title || extractTitle(markdown);
28
+ const signature = opts.from ? opts.from.trim().slice(0, 120) : '';
25
29
 
26
30
  const body = {
27
31
  mode: opts.public ? 'public' : 'encrypted',
28
32
  expireDays: opts.expire ?? null,
29
33
  sentTo,
34
+ mailMessage: opts.mailmessage ? opts.mailmessage.trim().slice(0, 1000) : '',
35
+ signature,
30
36
  sourceSession: process.env.CLAUDE_SESSION_ID || null,
31
37
  sourceCwd: process.cwd(),
32
38
  };
@@ -70,7 +76,16 @@ export async function shot(args) {
70
76
  }
71
77
 
72
78
  function parseArgs(args) {
73
- const opts = { file: null, public: false, copy: false, tomail: undefined, expire: null, title: null };
79
+ const opts = {
80
+ file: null,
81
+ public: false,
82
+ copy: false,
83
+ mailto: undefined,
84
+ mailmessage: null,
85
+ expire: null,
86
+ title: null,
87
+ from: null,
88
+ };
74
89
  for (let i = 0; i < args.length; i++) {
75
90
  const a = args[i];
76
91
  switch (a) {
@@ -80,11 +95,17 @@ function parseArgs(args) {
80
95
  case '--copy':
81
96
  opts.copy = true;
82
97
  break;
83
- case '--tomail':
98
+ case '--mailto':
84
99
  if (args[i + 1] && !args[i + 1].startsWith('-')) {
85
- opts.tomail = args[++i];
100
+ opts.mailto = args[++i];
86
101
  } else {
87
- opts.tomail = null; // use defaultToMail from the config
102
+ opts.mailto = null; // use defaultToMail from the config
103
+ }
104
+ break;
105
+ case '--mailmessage':
106
+ opts.mailmessage = args[++i];
107
+ if (opts.mailmessage === undefined || opts.mailmessage.startsWith('-')) {
108
+ throw new Error('--mailmessage requires a value');
88
109
  }
89
110
  break;
90
111
  case '--expire': {
@@ -99,6 +120,10 @@ function parseArgs(args) {
99
120
  opts.title = args[++i];
100
121
  if (opts.title === undefined) throw new Error('--title requires a value');
101
122
  break;
123
+ case '--from':
124
+ opts.from = args[++i];
125
+ if (opts.from === undefined || opts.from.startsWith('-')) throw new Error('--from requires a value');
126
+ break;
102
127
  default:
103
128
  if (a.startsWith('-')) throw new Error(`unknown flag "${a}". See: slingit help`);
104
129
  if (opts.file) throw new Error('only one file can be given');