you-wrapped 0.2.0 → 0.3.0
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/SKILL.md +35 -0
- package/bin/cli.mjs +5 -2
- package/package.json +1 -1
- package/src/discover.mjs +86 -0
- package/src/prompt.mjs +2 -0
package/SKILL.md
CHANGED
|
@@ -88,6 +88,41 @@ npx you-wrapped --render portrait.json --out ./wrapped
|
|
|
88
88
|
Produces `wrapped.html` and a redacted `wrapped.share.html`. Tell them to read the share
|
|
89
89
|
copy before posting it — they should verify their own scrub.
|
|
90
90
|
|
|
91
|
+
## Phase 5 — offer to go deeper (every machine is different)
|
|
92
|
+
|
|
93
|
+
The standard sweep is a floor, not a ceiling. `signals.json` contains an `unexplored`
|
|
94
|
+
block listing sources that were **detected on this machine but deliberately not read** —
|
|
95
|
+
plus a folder inventory. Use it to make a *specific* offer, never a generic menu.
|
|
96
|
+
|
|
97
|
+
After delivering the portrait, tell them what else is available and what it would add.
|
|
98
|
+
Name the actual apps found, with a one-line reason each:
|
|
99
|
+
|
|
100
|
+
> "There's more here if you want it. I found Obsidian (24 MB of notes), WhatsApp,
|
|
101
|
+
> Signal, a Lightroom catalog, and 175 MB of your past Claude sessions. Your notes
|
|
102
|
+
> would sharpen the private-voice read; WhatsApp is often where family lives; the
|
|
103
|
+
> Claude sessions show what you actually ask for help with. Want any of those?"
|
|
104
|
+
|
|
105
|
+
Also offer, when relevant:
|
|
106
|
+
|
|
107
|
+
- **Other connectors** they have but you didn't sweep — Linear, GitHub, Notion, Figma,
|
|
108
|
+
Spotify, Strava, banking exports. Check what's actually available before offering.
|
|
109
|
+
- **Deeper on one source** rather than wider: a single relationship over five years, one
|
|
110
|
+
project end-to-end, or their sent mail alone.
|
|
111
|
+
- **A specific question** they bring. The best second pass is usually answering something
|
|
112
|
+
they've wondered about themselves, not more breadth.
|
|
113
|
+
- **Their creative output** — the films, repos, designs, or writing they actually made.
|
|
114
|
+
Data about a person is not the same as the work they produced.
|
|
115
|
+
|
|
116
|
+
Rules for going deeper:
|
|
117
|
+
|
|
118
|
+
- **Read only what they ask for.** Detection is not permission. Never open a newly
|
|
119
|
+
discovered source without an explicit yes.
|
|
120
|
+
- **Say what each source would and wouldn't tell you** before they decide.
|
|
121
|
+
- **Re-render when you learn something material** — a portrait that ignores a major new
|
|
122
|
+
source is worse than one that never had it.
|
|
123
|
+
- If a source is encrypted at rest (Granola, Signal, some note apps), say so plainly and
|
|
124
|
+
point at the connector or export path instead of attempting to break it.
|
|
125
|
+
|
|
91
126
|
---
|
|
92
127
|
|
|
93
128
|
## Rules
|
package/bin/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { analyze } from '../src/analyze.mjs';
|
|
|
7
7
|
import { SYSTEM, buildUserPrompt } from '../src/prompt.mjs';
|
|
8
8
|
import { render } from '../src/render.mjs';
|
|
9
9
|
import { scrubDeep, coarsenPlaces, harvestNames, labelFor } from '../src/scrub.mjs';
|
|
10
|
+
import { discover } from '../src/discover.mjs';
|
|
10
11
|
import { installSkill, skillInstalled, checkFullDiskAccess, openPrivacyPane, hostApp, nodeOk, resolveInvocation, hasClaudeCLI, insideClaude, launchClaude } from '../src/setup.mjs';
|
|
11
12
|
|
|
12
13
|
const args = process.argv.slice(2);
|
|
@@ -150,7 +151,9 @@ photos.ok ? done(`photos — ${photos.count.toLocaleString()} (metadata only, no
|
|
|
150
151
|
// ── 2. analyze
|
|
151
152
|
step('analyzing');
|
|
152
153
|
const profile = analyze({ messages, notes, browser, photos }, { days: DAYS });
|
|
153
|
-
|
|
154
|
+
// what else is on this machine that we did NOT read — the agent offers these
|
|
155
|
+
profile.unexplored = discover();
|
|
156
|
+
done(`analyzed — ${profile.unexplored.apps.length} more sources detected but unread`);
|
|
154
157
|
|
|
155
158
|
// ── 3. evidence pack (stays local unless you synthesize)
|
|
156
159
|
const m = profile.sources.messages || {};
|
|
@@ -241,7 +244,7 @@ ${portrait ? ` ${c.d}wrapped.html${c.x} your portrait
|
|
|
241
244
|
${c.d}wrapped.share.html${c.x} redacted copy, safe to post` : ''}
|
|
242
245
|
|
|
243
246
|
${c.d}${statLine}${c.x}
|
|
244
|
-
`);
|
|
247
|
+
${profile.unexplored.apps.length ? `${c.d} ${profile.unexplored.apps.length} other sources on this machine were detected but not read.\n Ask Claude to go deeper and it will offer them.${c.x}\n` : ''}`);
|
|
245
248
|
|
|
246
249
|
// ── 6. hand off to Claude Code
|
|
247
250
|
// The skill was written to disk above, and a *new* claude process loads skills
|
package/package.json
CHANGED
package/src/discover.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Everyone's machine is different. This looks for sources the CLI does NOT read,
|
|
2
|
+
// so the agent can offer a specific "want to go deeper?" instead of a generic menu.
|
|
3
|
+
// Existence and size only — nothing here opens or parses a file.
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const H = homedir();
|
|
10
|
+
const p = (...x) => join(H, ...x);
|
|
11
|
+
|
|
12
|
+
const CANDIDATES = [
|
|
13
|
+
// note & knowledge apps
|
|
14
|
+
{ id: 'obsidian', label: 'Obsidian vault', path: p('Library/Application Support/obsidian'), why: 'long-form notes and daily journals' },
|
|
15
|
+
{ id: 'notion', label: 'Notion (local cache)', path: p('Library/Application Support/Notion'), why: 'docs and databases you own' },
|
|
16
|
+
{ id: 'bear', label: 'Bear notes', path: p('Library/Group Containers/9K33E3U3T4.net.shinyfrog.bear'), why: 'personal notes' },
|
|
17
|
+
{ id: 'craft', label: 'Craft', path: p('Library/Containers/com.lukilabs.lukiapp'), why: 'documents' },
|
|
18
|
+
{ id: 'devonthink',label: 'DEVONthink', path: p('Library/Application Support/DEVONthink 3'), why: 'research archive' },
|
|
19
|
+
|
|
20
|
+
// meetings & comms
|
|
21
|
+
{ id: 'granola', label: 'Granola', path: p('Library/Application Support/Granola'), why: 'meeting notes — what you say out loud (encrypted locally; use the MCP connector)' },
|
|
22
|
+
{ id: 'slack', label: 'Slack (desktop)', path: p('Library/Application Support/Slack'), why: 'where many people ideate, distinct from where they decide' },
|
|
23
|
+
{ id: 'discord', label: 'Discord', path: p('Library/Application Support/discord'), why: 'community and side-project conversation' },
|
|
24
|
+
{ id: 'whatsapp', label: 'WhatsApp', path: p('Library/Group Containers/group.net.whatsapp.WhatsApp.shared'), why: 'often where family and non-US contacts live' },
|
|
25
|
+
{ id: 'signal', label: 'Signal', path: p('Library/Application Support/Signal'), why: 'private conversation' },
|
|
26
|
+
{ id: 'mail', label: 'Apple Mail', path: p('Library/Mail'), why: 'sent mail is a distinct professional voice' },
|
|
27
|
+
|
|
28
|
+
// creative & work output
|
|
29
|
+
{ id: 'lightroom', label: 'Lightroom catalog', path: p('Pictures/Lightroom Library.lrlibrary'), why: 'what you photograph, and how you edit it' },
|
|
30
|
+
{ id: 'photos', label: 'Photos library', path: p('Pictures/Photos Library.photoslibrary'), why: 'faces, places, and who you spend time with' },
|
|
31
|
+
{ id: 'resolve', label: 'DaVinci Resolve', path: p('Library/Application Support/Blackmagic Design/DaVinci Resolve'), why: 'projects and timelines you cut' },
|
|
32
|
+
{ id: 'xcode', label: 'Xcode', path: p('Library/Developer/Xcode'), why: 'what you build' },
|
|
33
|
+
{ id: 'music', label: 'Music library', path: p('Music/Music/Music Library.musiclibrary'), why: 'taste and listening rhythm' },
|
|
34
|
+
{ id: 'books', label: 'Apple Books', path: p('Library/Containers/com.apple.iBooksX'), why: 'what you read and highlight' },
|
|
35
|
+
|
|
36
|
+
// agent & shell history
|
|
37
|
+
{ id: 'claude', label: 'Past Claude sessions', path: p('.claude/projects'), why: 'what you actually ask for help with' },
|
|
38
|
+
{ id: 'cursor', label: 'Cursor history', path: p('Library/Application Support/Cursor'), why: 'how you work with AI in code' },
|
|
39
|
+
{ id: 'zsh', label: 'Shell history', path: p('.zsh_history'), why: 'the commands you actually run' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Folders worth naming if they're large — where someone's real output lives.
|
|
43
|
+
const FOLDERS = ['Desktop', 'Documents', 'Downloads', 'Movies', 'Music', 'Pictures', 'Developer', 'Projects', 'Code'];
|
|
44
|
+
|
|
45
|
+
function dirBytes(dir, depth = 0, budget = { n: 0 }) {
|
|
46
|
+
// Deliberately shallow: this is a size hint, not an audit.
|
|
47
|
+
if (depth > 2 || budget.n > 4000) return 0;
|
|
48
|
+
let total = 0;
|
|
49
|
+
let entries;
|
|
50
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return 0; }
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
if (budget.n++ > 4000) break;
|
|
53
|
+
if (e.name.startsWith('.')) continue;
|
|
54
|
+
const full = join(dir, e.name);
|
|
55
|
+
try {
|
|
56
|
+
if (e.isDirectory()) total += dirBytes(full, depth + 1, budget);
|
|
57
|
+
else total += statSync(full).size;
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
return total;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function discover() {
|
|
64
|
+
const found = [];
|
|
65
|
+
for (const c of CANDIDATES) {
|
|
66
|
+
if (!existsSync(c.path)) continue;
|
|
67
|
+
let size = 0;
|
|
68
|
+
try { size = statSync(c.path).isDirectory() ? dirBytes(c.path) : statSync(c.path).size; } catch {}
|
|
69
|
+
found.push({ id: c.id, label: c.label, why: c.why, approxBytes: size });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const folders = [];
|
|
73
|
+
for (const f of FOLDERS) {
|
|
74
|
+
const dir = p(f);
|
|
75
|
+
if (!existsSync(dir)) continue;
|
|
76
|
+
let count = 0;
|
|
77
|
+
try { count = readdirSync(dir).filter(x => !x.startsWith('.')).length; } catch {}
|
|
78
|
+
if (count > 0) folders.push({ name: f, entries: count });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
apps: found.sort((a, b) => b.approxBytes - a.approxBytes),
|
|
83
|
+
folders: folders.sort((a, b) => b.entries - a.entries),
|
|
84
|
+
note: 'Detected but NOT read. Offer these to the user; read only what they ask for.',
|
|
85
|
+
};
|
|
86
|
+
}
|
package/src/prompt.mjs
CHANGED
|
@@ -40,6 +40,8 @@ RULES:
|
|
|
40
40
|
ownership — those are the only two levers that move a capacity-constrained person.
|
|
41
41
|
- If the data is thin in some area, say so. An honest gap beats a confident fabrication.
|
|
42
42
|
- Match their voice. If their messages are short and profane, don't write them a corporate memo.
|
|
43
|
+
- After the portrait, offer to go deeper using the "unexplored" block in the signals —
|
|
44
|
+
name the specific sources found on THIS machine. Read nothing new without an explicit yes.
|
|
43
45
|
|
|
44
46
|
OUTPUT: a JSON object matching the schema you are given. No prose outside the JSON.`;
|
|
45
47
|
|