@cullumco/cadence 0.1.5 → 0.1.6
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/.claude-plugin/plugin.json +1 -1
- package/README.md +4 -4
- package/dist/cli.js +25 -9
- package/dist/config.js +1 -0
- package/dist/hook.js +4 -1
- package/dist/posttool.js +76 -12
- package/dist/providers/environment.js +44 -5
- package/dist/signals-view.js +7 -5
- package/dist/vibe.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,8 +67,8 @@ deliberate self-report just outranks it.
|
|
|
67
67
|
**Signals → dials → a reframe lens.**
|
|
68
68
|
|
|
69
69
|
1. **Signals** — what Cadence can sense right now:
|
|
70
|
-
- **
|
|
71
|
-
uptime/load, dark mode, displays, wifi, Focus/DND. Mostly zero-setup;
|
|
70
|
+
- **environment** — time of day, day of week, weather (opt-in), battery, machine
|
|
71
|
+
uptime/load, dark mode, displays, wifi (opt-in), Focus/DND. Mostly zero-setup;
|
|
72
72
|
time/day work everywhere, the Mac-context probes are macOS. The one signal
|
|
73
73
|
that's always there: `context: friday afternoon, rainy, focus on`.
|
|
74
74
|
(Focus detection reads the DND database directly, so it needs your
|
|
@@ -112,7 +112,7 @@ without checking in stays your call, not the soundtrack's.
|
|
|
112
112
|
## Requirements
|
|
113
113
|
|
|
114
114
|
- **Built for the Mac.** The richest ambient probes — music via AppleScript
|
|
115
|
-
now-playing, battery, dark mode, displays, wifi, Focus/DND, focused app —
|
|
115
|
+
now-playing, battery, dark mode, displays, wifi (opt-in), Focus/DND, focused app —
|
|
116
116
|
read the Mac around you. On other platforms Cadence still runs and still
|
|
117
117
|
moves the dials: prompt intent, self-report, git, time/day, and typing tempo
|
|
118
118
|
work anywhere, Spotify can be linked for music, and the Mac-only probes
|
|
@@ -285,7 +285,7 @@ See [`BACKLOG.md`](BACKLOG.md). Highlights:
|
|
|
285
285
|
## Caveats
|
|
286
286
|
|
|
287
287
|
- **Built for the Mac.** The richest ambient probes (music-via-OS, battery,
|
|
288
|
-
dark mode, displays, wifi, Focus, focused app) are macOS. Other platforms
|
|
288
|
+
dark mode, displays, wifi (opt-in), Focus, focused app) are macOS. Other platforms
|
|
289
289
|
keep the dial-movers — intent, self-report, git, time/day, typing tempo,
|
|
290
290
|
linked Spotify — and the rest degrades silently.
|
|
291
291
|
- **Spotify's audio-features API is not used** — Spotify deprecated it for new
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { writeFile, mkdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { getMusicSignal } from "./providers/music.js";
|
|
6
|
-
import { getSelfReportSignal } from "./providers/selfreport.js";
|
|
6
|
+
import { getSelfReportSignal, STALE_AFTER_MS } from "./providers/selfreport.js";
|
|
7
7
|
import { getEnvironmentSignal } from "./providers/environment.js";
|
|
8
8
|
import { getGitSignal } from "./providers/git.js";
|
|
9
9
|
import { getEsotericSignal } from "./providers/esoteric.js";
|
|
@@ -17,24 +17,36 @@ const STATE_FILE = join(CADENCE_DIR, "state.txt");
|
|
|
17
17
|
const CONFIG_FILE = join(CADENCE_DIR, "config.json");
|
|
18
18
|
async function cmdReport(args) {
|
|
19
19
|
if (args.length === 0) {
|
|
20
|
+
// Same TTL the hook applies — printing an expired report as if it were
|
|
21
|
+
// live would contradict what actually gets injected.
|
|
22
|
+
const report = await getSelfReportSignal();
|
|
23
|
+
if (report) {
|
|
24
|
+
const rem = Math.max(0, STALE_AFTER_MS - (Date.now() - report.setAt));
|
|
25
|
+
const h = Math.floor(rem / 3_600_000);
|
|
26
|
+
const m = Math.floor((rem % 3_600_000) / 60_000);
|
|
27
|
+
console.log(`${report.text} (${h > 0 ? `${h}h${String(m).padStart(2, "0")}m` : `${m}m`} left)`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
20
30
|
try {
|
|
21
|
-
const
|
|
22
|
-
console.log(
|
|
31
|
+
const stale = (await readFile(STATE_FILE, "utf-8")).trim();
|
|
32
|
+
console.log(stale
|
|
33
|
+
? `(last self-report expired — refresh: cadence report "...")`
|
|
34
|
+
: "(no self-report set)");
|
|
23
35
|
}
|
|
24
36
|
catch {
|
|
25
|
-
console.log("(no
|
|
37
|
+
console.log("(no self-report set)");
|
|
26
38
|
}
|
|
27
39
|
return;
|
|
28
40
|
}
|
|
29
41
|
const text = args.join(" ");
|
|
30
42
|
await mkdir(CADENCE_DIR, { recursive: true });
|
|
31
43
|
await writeFile(STATE_FILE, text, "utf-8");
|
|
32
|
-
console.log(`
|
|
44
|
+
console.log(` self-report set: "${text}"`);
|
|
33
45
|
}
|
|
34
46
|
async function cmdClear() {
|
|
35
47
|
await mkdir(CADENCE_DIR, { recursive: true });
|
|
36
48
|
await writeFile(STATE_FILE, "", "utf-8");
|
|
37
|
-
console.log("
|
|
49
|
+
console.log(" self-report cleared");
|
|
38
50
|
}
|
|
39
51
|
// Collects live signals and renders the exact block the hook would inject,
|
|
40
52
|
// or null when there's nothing to say. Shared by `test` and the bare command.
|
|
@@ -46,6 +58,7 @@ async function buildPreview() {
|
|
|
46
58
|
getSelfReportSignal().catch(() => null),
|
|
47
59
|
getEnvironmentSignal(new Date(), {
|
|
48
60
|
focusedAppEnabled: providerEnabled(providers, "focusedApp"),
|
|
61
|
+
wifiEnabled: providerEnabled(providers, "wifi"),
|
|
49
62
|
}).catch(() => null),
|
|
50
63
|
getGitSignal(process.cwd()).catch(() => null),
|
|
51
64
|
getEsotericSignal(providers).catch(() => null),
|
|
@@ -79,12 +92,15 @@ async function cmdTest() {
|
|
|
79
92
|
// The legibility view: every signal Cadence can read — live value, or the
|
|
80
93
|
// reason it's absent. Unlike `test`, this never goes silent.
|
|
81
94
|
async function cmdSignals() {
|
|
82
|
-
const
|
|
95
|
+
const providers = await loadProviders();
|
|
96
|
+
const [music, report, environment, git] = await Promise.all([
|
|
83
97
|
getMusicSignal().catch(() => null),
|
|
84
98
|
getSelfReportSignal().catch(() => null),
|
|
85
|
-
getEnvironmentSignal(new Date()
|
|
99
|
+
getEnvironmentSignal(new Date(), {
|
|
100
|
+
focusedAppEnabled: providerEnabled(providers, "focusedApp"),
|
|
101
|
+
wifiEnabled: providerEnabled(providers, "wifi"),
|
|
102
|
+
}).catch(() => null),
|
|
86
103
|
getGitSignal(process.cwd()).catch(() => null),
|
|
87
|
-
loadProviders(),
|
|
88
104
|
]);
|
|
89
105
|
console.log("\n" +
|
|
90
106
|
renderSignalsTable({
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,7 @@ const CONFIG_FILE = join(homedir(), ".cadence", "config.json");
|
|
|
24
24
|
export const OPT_IN_PROVIDERS = {
|
|
25
25
|
typingTempo: "prompt rhythm — rapid-fire vs. one long considered prompt → pace",
|
|
26
26
|
focusedApp: "frontmost non-terminal app (macOS) → flavor in the context line",
|
|
27
|
+
wifi: "wifi network name (macOS) → place context, home vs. office vs. café",
|
|
27
28
|
moon: "current moon phase (offline) → esoteric flavor",
|
|
28
29
|
horoscope: "daily horoscope for your sign, e.g. `cadence enable horoscope leo`",
|
|
29
30
|
};
|
package/dist/hook.js
CHANGED
|
@@ -35,7 +35,10 @@ async function collectSignals(cwd, prompt, providers) {
|
|
|
35
35
|
const [music, report, environment, git, activity, intent, esoteric] = await Promise.allSettled([
|
|
36
36
|
getMusicSignal(providers),
|
|
37
37
|
getSelfReportSignal(),
|
|
38
|
-
getEnvironmentSignal(new Date(), {
|
|
38
|
+
getEnvironmentSignal(new Date(), {
|
|
39
|
+
focusedAppEnabled: providerEnabled(providers, "focusedApp"),
|
|
40
|
+
wifiEnabled: providerEnabled(providers, "wifi"),
|
|
41
|
+
}),
|
|
39
42
|
getGitSignal(cwd),
|
|
40
43
|
getActivitySignal(prompt, Date.now(), { tempoEnabled }),
|
|
41
44
|
getIntentSignal(prompt),
|
package/dist/posttool.js
CHANGED
|
@@ -14,15 +14,17 @@ import { isPaused } from "./config.js";
|
|
|
14
14
|
* event: the repo entering or leaving a merge/rebase conflict — the
|
|
15
15
|
* strongest debug tell we have, far stronger than music ever was.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Three material events now, same discipline:
|
|
18
18
|
* 1. the repo entering/leaving a merge/rebase conflict (re-observed via git)
|
|
19
19
|
* 2. a streak of destructive git ops — reset --hard / force-push — read off
|
|
20
20
|
* the command string (no tool_response parsing), i.e. thrash.
|
|
21
|
+
* 3. the test suite entering/leaving a failing state — read off the output
|
|
22
|
+
* of test-runner commands, tri-state honest (can't tell → don't update).
|
|
21
23
|
*
|
|
22
24
|
* Discipline (the BACKLOG's hard constraint):
|
|
23
|
-
* - fires only after Bash tool calls whose command mentions git
|
|
24
|
-
* - injects at most once per TRANSITION (conflict edge,
|
|
25
|
-
* never per tool
|
|
25
|
+
* - fires only after Bash tool calls whose command mentions git or a test run
|
|
26
|
+
* - injects at most once per TRANSITION (conflict edge, thrash threshold,
|
|
27
|
+
* or tests-failing edge), never per tool
|
|
26
28
|
* - silent in every other case, silent on every error
|
|
27
29
|
* ───────────────────────────────────────────────────────────────────────── */
|
|
28
30
|
const TOTAL_BUDGET_MS = 1500;
|
|
@@ -30,14 +32,26 @@ const STATE_FILE = join(homedir(), ".cadence", "workstate.json");
|
|
|
30
32
|
const MAX_SESSIONS = 20;
|
|
31
33
|
const THRASH_WINDOW_MS = 10 * 60_000; // destructive ops within 10 min count as a streak
|
|
32
34
|
const THRASH_MIN = 2; // 2nd destructive op in the window = thrash
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
35
|
+
// Does this command run a test suite? Phrase-based like intent.ts — runner
|
|
36
|
+
// names and `npm test`-shaped invocations, not the bare word "test" (which
|
|
37
|
+
// would match `git stash list | grep test`). Pure + exported for tests.
|
|
38
|
+
export function isTestCommand(cmd) {
|
|
39
|
+
return (/\b(?:npm|yarn|pnpm|bun)\s+(?:run\s+)?test\b/.test(cmd) ||
|
|
40
|
+
/\bnode\s+--test\b/.test(cmd) ||
|
|
41
|
+
/\b(?:jest|vitest|mocha|pytest|tape|ava)\b/.test(cmd) ||
|
|
42
|
+
/\bgo\s+test\b/.test(cmd) ||
|
|
43
|
+
/\bcargo\s+test\b/.test(cmd));
|
|
44
|
+
}
|
|
45
|
+
// Gate 1: is this tool call even capable of changing what we observe?
|
|
46
|
+
// Only Bash commands that mention git (conflict/thrash) or run tests —
|
|
47
|
+
// checking after every tool call would betray the silence rule.
|
|
36
48
|
export function shouldCheck(input) {
|
|
37
49
|
if (input.tool_name !== "Bash")
|
|
38
50
|
return false;
|
|
39
51
|
const cmd = input.tool_input?.command;
|
|
40
|
-
|
|
52
|
+
if (typeof cmd !== "string")
|
|
53
|
+
return false;
|
|
54
|
+
return /\bgit\b/.test(cmd) || isTestCommand(cmd);
|
|
41
55
|
}
|
|
42
56
|
// Gate 2: did the observed state actually TRANSITION? Speak only on the edge.
|
|
43
57
|
// undefined → true first observation reveals a conflict → speak
|
|
@@ -59,6 +73,50 @@ export function refineContext(prev, conflicted) {
|
|
|
59
73
|
}
|
|
60
74
|
return null; // first observation of a clean repo: record silently
|
|
61
75
|
}
|
|
76
|
+
/* Did the test run fail? Tri-state honest, read off the runner's own summary:
|
|
77
|
+
* true a nonzero failure count, or an unambiguous failure marker
|
|
78
|
+
* false an explicit zero-failure count, or passes with no failure marks
|
|
79
|
+
* undefined can't tell → caller must NOT update state (couldn't look ≠ ok)
|
|
80
|
+
* Counts beat markers: "✖ 0 failing" is a pass even though ✖ appears. */
|
|
81
|
+
export function testsFailedFrom(resp) {
|
|
82
|
+
if (resp == null)
|
|
83
|
+
return undefined;
|
|
84
|
+
const text = typeof resp === "string" ? resp : JSON.stringify(resp);
|
|
85
|
+
if (!text)
|
|
86
|
+
return undefined;
|
|
87
|
+
// explicit counts, either word order: "2 failed" / "fail 2" / "failures: 0"
|
|
88
|
+
const counts = [
|
|
89
|
+
...text.matchAll(/(\d+)\s*(?:tests?\s+)?fail(?:ed|ing|ures?)?\b/gi),
|
|
90
|
+
...text.matchAll(/\bfail(?:ed|ing|ures?)?[:\s]+(\d+)/gi),
|
|
91
|
+
].map((m) => Number(m[1]));
|
|
92
|
+
if (counts.some((n) => n > 0))
|
|
93
|
+
return true;
|
|
94
|
+
if (counts.length > 0)
|
|
95
|
+
return false;
|
|
96
|
+
// unambiguous markers (go test FAIL, TAP "not ok", node/jest ✖ lists)
|
|
97
|
+
if (/\bFAIL(?:ED)?\b/.test(text) || /\bnot ok\b/.test(text) || /✖/.test(text))
|
|
98
|
+
return true;
|
|
99
|
+
// passes with no failure marks anywhere
|
|
100
|
+
if (/\b\d+\s+pass(?:ed|ing)?\b/i.test(text) || /\bok\b/.test(text))
|
|
101
|
+
return false;
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
// Same edge contract as refineContext, for the tests-failing state.
|
|
105
|
+
export function refineTests(prev, failing) {
|
|
106
|
+
if (prev === failing)
|
|
107
|
+
return null;
|
|
108
|
+
if (failing) {
|
|
109
|
+
return ("<user_state_update>observed work: the test suite just started failing. " +
|
|
110
|
+
"Read the cadence as debug now — verify before building further, and " +
|
|
111
|
+
"treat the failures as the current ground truth. If the user's words " +
|
|
112
|
+
"clearly mean otherwise, follow their words.</user_state_update>");
|
|
113
|
+
}
|
|
114
|
+
if (prev === true) {
|
|
115
|
+
return ("<user_state_update>observed work: the tests are passing again — drop " +
|
|
116
|
+
"the debug framing and return to the user's prior cadence.</user_state_update>");
|
|
117
|
+
}
|
|
118
|
+
return null; // first observation of a passing suite: record silently
|
|
119
|
+
}
|
|
62
120
|
// Is this command a destructive/undo git op? Just `reset --hard` and a true
|
|
63
121
|
// force-push — NOT --force-with-lease (the safe one), checkout, or restore
|
|
64
122
|
// (too ordinary to read as thrash). Pure + exported for tests.
|
|
@@ -138,10 +196,15 @@ async function main() {
|
|
|
138
196
|
const state = await loadState();
|
|
139
197
|
const prev = state[key];
|
|
140
198
|
const now = Date.now();
|
|
199
|
+
const cmd = typeof input.tool_input?.command === "string" ? input.tool_input.command : "";
|
|
141
200
|
// 1. conflict edge (re-observed via git)
|
|
142
201
|
const conflictMsg = refineContext(prev?.conflicted, git.conflicted);
|
|
143
|
-
// 2.
|
|
144
|
-
|
|
202
|
+
// 2. tests-failing edge (read off the runner's output; tri-state honest —
|
|
203
|
+
// an unreadable run keeps the previous observation, it never clears it)
|
|
204
|
+
const failed = isTestCommand(cmd) ? testsFailedFrom(input.tool_response) : undefined;
|
|
205
|
+
const testsFailing = failed ?? prev?.testsFailing;
|
|
206
|
+
const testsMsg = failed != null ? refineTests(prev?.testsFailing, failed) : null;
|
|
207
|
+
// 3. thrash threshold (read off the command string)
|
|
145
208
|
const times = prev?.thrashTimes ?? [];
|
|
146
209
|
const nextTimes = isThrashCommand(cmd) ? [...times, now] : times;
|
|
147
210
|
const thrash = refineThrash(nextTimes, now, prev?.thrashAnnounced ?? false);
|
|
@@ -150,10 +213,11 @@ async function main() {
|
|
|
150
213
|
at: now,
|
|
151
214
|
thrashTimes: thrash.times,
|
|
152
215
|
thrashAnnounced: thrash.announced,
|
|
216
|
+
...(testsFailing != null ? { testsFailing } : {}),
|
|
153
217
|
};
|
|
154
218
|
await saveState(state);
|
|
155
|
-
//
|
|
156
|
-
const message = conflictMsg ?? thrash.message;
|
|
219
|
+
// Strongest tell wins: conflict > tests > thrash. At most one message.
|
|
220
|
+
const message = conflictMsg ?? testsMsg ?? thrash.message;
|
|
157
221
|
if (message) {
|
|
158
222
|
process.stdout.write(JSON.stringify({
|
|
159
223
|
hookSpecificOutput: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
3
|
import { homedir, uptime, loadavg, cpus } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
// One-liner shell helper for the best-effort macOS probes. Always resolves
|
|
@@ -155,12 +155,16 @@ export async function getFocusedApp(now) {
|
|
|
155
155
|
return name;
|
|
156
156
|
}
|
|
157
157
|
// ── mac context: best-effort shell-outs, all flavor (no dial nudges) ─────────
|
|
158
|
-
async function getMacContext(focusedAppEnabled) {
|
|
158
|
+
async function getMacContext(focusedAppEnabled, wifiEnabled) {
|
|
159
159
|
if (process.platform !== "darwin")
|
|
160
160
|
return {};
|
|
161
161
|
const [dark, ssid, displays, focus, focusedApp] = await Promise.all([
|
|
162
162
|
sh("defaults read -g AppleInterfaceStyle"), // "Dark", or error (=light)
|
|
163
|
-
|
|
163
|
+
// SSID names your location — opt-in (2026-06-11), like everything
|
|
164
|
+
// privacy-adjacent. Off → don't even spawn the probe.
|
|
165
|
+
wifiEnabled
|
|
166
|
+
? sh("ipconfig getsummary en0 | awk -F ' SSID : ' '/ SSID : / {print $2}'", 700)
|
|
167
|
+
: Promise.resolve(null),
|
|
164
168
|
// fast display count via AppleScript (~100ms) — NOT system_profiler (1-3s)
|
|
165
169
|
sh(`osascript -e 'tell application "System Events" to count of desktops'`, 700),
|
|
166
170
|
getFocus(),
|
|
@@ -198,6 +202,21 @@ function weatherWord(code) {
|
|
|
198
202
|
return "snowy";
|
|
199
203
|
return "stormy";
|
|
200
204
|
}
|
|
205
|
+
const WEATHER_CACHE_FILE = join(homedir(), ".cadence", "weather-cache.json");
|
|
206
|
+
// Weather barely moves in half an hour, and the hook + stop hook both ask —
|
|
207
|
+
// without a cache that's two Open-Meteo round-trips per turn for one word.
|
|
208
|
+
const WEATHER_CACHE_MS = 30 * 60_000;
|
|
209
|
+
// Pure freshness check, exported for tests: same location, younger than TTL.
|
|
210
|
+
export function weatherCacheFresh(c, lat, lon, now) {
|
|
211
|
+
if (!c || typeof c !== "object")
|
|
212
|
+
return false;
|
|
213
|
+
const w = c;
|
|
214
|
+
return (typeof w.word === "string" &&
|
|
215
|
+
typeof w.at === "number" &&
|
|
216
|
+
w.lat === lat &&
|
|
217
|
+
w.lon === lon &&
|
|
218
|
+
now - w.at <= WEATHER_CACHE_MS);
|
|
219
|
+
}
|
|
201
220
|
async function getWeather() {
|
|
202
221
|
let cfg;
|
|
203
222
|
try {
|
|
@@ -209,6 +228,14 @@ async function getWeather() {
|
|
|
209
228
|
const loc = cfg.location;
|
|
210
229
|
if (!loc || typeof loc.lat !== "number" || typeof loc.lon !== "number")
|
|
211
230
|
return undefined;
|
|
231
|
+
try {
|
|
232
|
+
const cached = JSON.parse(await readFile(WEATHER_CACHE_FILE, "utf-8"));
|
|
233
|
+
if (weatherCacheFresh(cached, loc.lat, loc.lon, Date.now()))
|
|
234
|
+
return cached.word;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// no cache yet → fetch below
|
|
238
|
+
}
|
|
212
239
|
const ctrl = new AbortController();
|
|
213
240
|
const timer = setTimeout(() => ctrl.abort(), WEATHER_TIMEOUT_MS);
|
|
214
241
|
try {
|
|
@@ -219,7 +246,19 @@ async function getWeather() {
|
|
|
219
246
|
return undefined;
|
|
220
247
|
const data = (await res.json());
|
|
221
248
|
const code = data.current?.weather_code;
|
|
222
|
-
|
|
249
|
+
if (typeof code !== "number")
|
|
250
|
+
return undefined;
|
|
251
|
+
const word = weatherWord(code);
|
|
252
|
+
try {
|
|
253
|
+
// best-effort cache write; a miss just means we fetch again next prompt
|
|
254
|
+
await mkdir(join(homedir(), ".cadence"), { recursive: true });
|
|
255
|
+
const cache = { word, at: Date.now(), lat: loc.lat, lon: loc.lon };
|
|
256
|
+
await writeFile(WEATHER_CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
return word;
|
|
223
262
|
}
|
|
224
263
|
catch {
|
|
225
264
|
return undefined;
|
|
@@ -235,7 +274,7 @@ export async function getEnvironmentSignal(now, opts = {}) {
|
|
|
235
274
|
const [weather, battery, mac] = await Promise.all([
|
|
236
275
|
getWeather(),
|
|
237
276
|
getBattery(),
|
|
238
|
-
getMacContext(opts.focusedAppEnabled ?? false),
|
|
277
|
+
getMacContext(opts.focusedAppEnabled ?? false, opts.wifiEnabled ?? false),
|
|
239
278
|
]);
|
|
240
279
|
return {
|
|
241
280
|
source: "environment",
|
package/dist/signals-view.js
CHANGED
|
@@ -15,7 +15,7 @@ function ttlLeft(setAt, now) {
|
|
|
15
15
|
const m = Math.floor((rem % 3_600_000) / 60_000);
|
|
16
16
|
return h > 0 ? `${h}h${String(m).padStart(2, "0")}m left` : `${m}m left`;
|
|
17
17
|
}
|
|
18
|
-
function environmentRows(a, platform) {
|
|
18
|
+
function environmentRows(a, platform, providers) {
|
|
19
19
|
if (!a)
|
|
20
20
|
return [top("environment", "— unavailable")];
|
|
21
21
|
const mac = platform === "darwin";
|
|
@@ -46,9 +46,11 @@ function environmentRows(a, platform) {
|
|
|
46
46
|
: row("displays", String(a.displays), "(hidden: only shows >1)"));
|
|
47
47
|
lines.push(!mac
|
|
48
48
|
? row("wifi", macNote)
|
|
49
|
-
:
|
|
50
|
-
? row("wifi",
|
|
51
|
-
:
|
|
49
|
+
: !providerEnabled(providers, "wifi")
|
|
50
|
+
? row("wifi", "— off (run: cadence enable wifi)")
|
|
51
|
+
: a.network
|
|
52
|
+
? row("wifi", JSON.stringify(a.network))
|
|
53
|
+
: row("wifi", "— unavailable (macOS may require Location Services)"));
|
|
52
54
|
lines.push(a.uptimeHours == null
|
|
53
55
|
? row("uptime", "— unavailable")
|
|
54
56
|
: a.uptimeHours >= 12
|
|
@@ -123,7 +125,7 @@ function optInFlavorRows(providers, environment) {
|
|
|
123
125
|
export function renderSignalsTable(raw) {
|
|
124
126
|
const providers = raw.providers ?? {};
|
|
125
127
|
return [
|
|
126
|
-
...environmentRows(raw.environment, raw.platform),
|
|
128
|
+
...environmentRows(raw.environment, raw.platform, raw.providers ?? {}),
|
|
127
129
|
...musicRows(raw.music, providers),
|
|
128
130
|
reportRow(raw.report, raw.now),
|
|
129
131
|
intentRow(),
|
package/dist/vibe.js
CHANGED
|
@@ -113,7 +113,7 @@ const GENRE_AFFECT = {
|
|
|
113
113
|
opera: { energy: 0.5, valence: 0.45, acoustic: 0.9, moods: ["epic"] },
|
|
114
114
|
choral: { energy: 0.3, valence: 0.5, acoustic: 0.95, moods: ["ethereal", "epic"] },
|
|
115
115
|
// ── low energy, low valence → pace low; dark moods are render-only ───
|
|
116
|
-
// (valence currently moves NO dial — see BACKLOG "
|
|
116
|
+
// (valence currently moves NO dial — see BACKLOG "Valence boundary")
|
|
117
117
|
blues: { energy: 0.45, valence: 0.35, acoustic: 0.5, moods: ["sad", "dark"] },
|
|
118
118
|
goth: { energy: 0.55, valence: 0.3, acoustic: 0.2, moods: ["dark"] },
|
|
119
119
|
gothic: { energy: 0.55, valence: 0.3, acoustic: 0.2, moods: ["dark"] },
|
package/package.json
CHANGED