@llblab/pi-kit 0.3.2 → 0.4.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/AGENTS.md +3 -3
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +19 -10
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
- package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-clean-room/README.md +61 -0
- package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
- package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
- package/node_modules/@llblab/pi-clean-room/package.json +53 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
- package/node_modules/@llblab/pi-state-flow/README.md +197 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/package.json +55 -0
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +2 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
- package/node_modules/@llblab/pi-telegram/index.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
- package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
- package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
- package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
- package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
- package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
- package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
- package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
- package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
- package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
- package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
- package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
- package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
- package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
- package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
- package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
- package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
- package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
- package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/package.json +46 -0
- package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
- package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
- package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
- package/package.json +20 -8
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
|
-
"description": "Version-pinned distribution of LLB Lab extensions for Pi",
|
|
8
|
+
"description": "Version-pinned distribution of LLB Lab extensions and Skills for Pi",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"pi-package",
|
|
11
11
|
"pi",
|
|
@@ -40,34 +40,46 @@
|
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@llblab/pi-actors": "0.52.
|
|
43
|
+
"@llblab/pi-actors": "0.52.1",
|
|
44
|
+
"@llblab/pi-clean-room": "0.1.1",
|
|
44
45
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
45
|
-
"@llblab/pi-grow-loop": "0.7.
|
|
46
|
-
"@llblab/pi-
|
|
46
|
+
"@llblab/pi-grow-loop": "0.7.4",
|
|
47
|
+
"@llblab/pi-state-flow": "0.3.0",
|
|
48
|
+
"@llblab/pi-telegram": "0.42.4",
|
|
49
|
+
"@llblab/skills": "1.14.0"
|
|
47
50
|
},
|
|
48
51
|
"bundledDependencies": [
|
|
49
52
|
"@llblab/pi-actors",
|
|
53
|
+
"@llblab/pi-clean-room",
|
|
50
54
|
"@llblab/pi-codex-usage",
|
|
51
55
|
"@llblab/pi-grow-loop",
|
|
52
|
-
"@llblab/pi-
|
|
56
|
+
"@llblab/pi-state-flow",
|
|
57
|
+
"@llblab/pi-telegram",
|
|
58
|
+
"@llblab/skills"
|
|
53
59
|
],
|
|
54
60
|
"pi": {
|
|
55
61
|
"extensions": [
|
|
56
62
|
"./node_modules/@llblab/pi-actors/dist/pi-actors/index.js",
|
|
63
|
+
"./node_modules/@llblab/pi-clean-room/index.ts",
|
|
57
64
|
"./node_modules/@llblab/pi-codex-usage/index.ts",
|
|
58
65
|
"./node_modules/@llblab/pi-grow-loop/index.ts",
|
|
66
|
+
"./node_modules/@llblab/pi-state-flow/index.ts",
|
|
59
67
|
"./node_modules/@llblab/pi-telegram/index.ts"
|
|
60
68
|
],
|
|
61
69
|
"skills": [
|
|
62
70
|
"./node_modules/@llblab/pi-actors/dist/skills",
|
|
63
71
|
"./node_modules/@llblab/pi-grow-loop/skills",
|
|
64
|
-
"./node_modules/@llblab/pi-telegram/skills"
|
|
72
|
+
"./node_modules/@llblab/pi-telegram/skills",
|
|
73
|
+
"./node_modules/@llblab/skills/"
|
|
65
74
|
]
|
|
66
75
|
},
|
|
67
76
|
"bundleDependencies": [
|
|
68
77
|
"@llblab/pi-actors",
|
|
78
|
+
"@llblab/pi-clean-room",
|
|
69
79
|
"@llblab/pi-codex-usage",
|
|
70
80
|
"@llblab/pi-grow-loop",
|
|
71
|
-
"@llblab/pi-
|
|
81
|
+
"@llblab/pi-state-flow",
|
|
82
|
+
"@llblab/pi-telegram",
|
|
83
|
+
"@llblab/skills"
|
|
72
84
|
]
|
|
73
85
|
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Actor-neutral client for the local Music Player playback protocol.
|
|
5
|
-
*
|
|
6
|
-
* Reads portable JSON status and sends bounded generation-fenced commands to
|
|
7
|
-
* the playback service endpoint without reading pi-actors Run or Control state.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
-
import { createConnection } from "node:net";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
const ACTIONS = new Set([
|
|
16
|
-
"play",
|
|
17
|
-
"resume",
|
|
18
|
-
"pause",
|
|
19
|
-
"toggle",
|
|
20
|
-
"next",
|
|
21
|
-
"previous",
|
|
22
|
-
"seek",
|
|
23
|
-
"volume",
|
|
24
|
-
"stop",
|
|
25
|
-
"status",
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function fail(message, code = 1) {
|
|
29
|
-
console.error(`music-player: ${message}`);
|
|
30
|
-
process.exit(code);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function expandPath(value) {
|
|
34
|
-
if (value === "~") return homedir();
|
|
35
|
-
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function readJson(path, fallback) {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
42
|
-
} catch {
|
|
43
|
-
return fallback;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parsePercent(value, label) {
|
|
48
|
-
if (!/^\d+$/.test(String(value))) {
|
|
49
|
-
fail(`${label} percent must be an integer 0..100`, 2);
|
|
50
|
-
}
|
|
51
|
-
const percent = Number(value);
|
|
52
|
-
if (percent < 0 || percent > 100) {
|
|
53
|
-
fail(`${label} percent must be an integer 0..100`, 2);
|
|
54
|
-
}
|
|
55
|
-
return percent;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function projectCurrentProgress(status, nowMs = Date.now()) {
|
|
59
|
-
const duration = Number(status.duration_seconds);
|
|
60
|
-
let position = Number(status.position_seconds);
|
|
61
|
-
const updatedAtMs = Number(status.position_updated_at_ms);
|
|
62
|
-
if (!Number.isFinite(position)) position = 0;
|
|
63
|
-
if (status.state === "playing" && Number.isFinite(updatedAtMs)) {
|
|
64
|
-
position += Math.max(0, nowMs - updatedAtMs) / 1000;
|
|
65
|
-
}
|
|
66
|
-
if (!Number.isFinite(duration) || duration <= 0) {
|
|
67
|
-
return { ...status, progress_percent: null };
|
|
68
|
-
}
|
|
69
|
-
position = Math.min(Math.max(position, 0), duration);
|
|
70
|
-
return {
|
|
71
|
-
...status,
|
|
72
|
-
progress_percent: Math.round((position / duration) * 100),
|
|
73
|
-
position_seconds: position,
|
|
74
|
-
position_updated_at_ms: nowMs,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function sendCommand(endpoint, action, input) {
|
|
79
|
-
const payload = `${JSON.stringify({
|
|
80
|
-
action,
|
|
81
|
-
...(input !== undefined ? { input } : {}),
|
|
82
|
-
service_instance_id: endpoint.service_instance_id,
|
|
83
|
-
})}\n`;
|
|
84
|
-
const response = await new Promise((resolveResponse, rejectResponse) => {
|
|
85
|
-
const socket = createConnection(endpoint.path);
|
|
86
|
-
let content = "";
|
|
87
|
-
const timeout = setTimeout(() => {
|
|
88
|
-
socket.destroy();
|
|
89
|
-
rejectResponse(new Error("playback service command timed out"));
|
|
90
|
-
}, 5_000);
|
|
91
|
-
timeout.unref?.();
|
|
92
|
-
socket.setEncoding("utf8");
|
|
93
|
-
socket.on("connect", () => socket.write(payload));
|
|
94
|
-
socket.on("data", (chunk) => {
|
|
95
|
-
content += chunk;
|
|
96
|
-
if (Buffer.byteLength(content, "utf8") > 4096) {
|
|
97
|
-
socket.destroy(new Error("playback service response is too large"));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
socket.on("error", rejectResponse);
|
|
101
|
-
socket.on("end", () => {
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
try {
|
|
104
|
-
resolveResponse(JSON.parse(content.trim()));
|
|
105
|
-
} catch {
|
|
106
|
-
rejectResponse(new Error("playback service returned invalid JSON"));
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
if (!response?.ok) {
|
|
111
|
-
throw new Error(response?.error || "playback service rejected the command");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const [action = "status", rawStateDir, ...actionArgs] = process.argv.slice(2);
|
|
116
|
-
if (!ACTIONS.has(action) || !rawStateDir) {
|
|
117
|
-
fail("usage: playback-client.mjs <status|play|resume|pause|toggle|next|previous|seek|volume|stop> <state-dir> [action args]", 2);
|
|
118
|
-
}
|
|
119
|
-
const stateDir = expandPath(rawStateDir);
|
|
120
|
-
if (action === "status") {
|
|
121
|
-
const status = readJson(join(stateDir, "player.json"), { state: "unknown" });
|
|
122
|
-
process.stdout.write(`${JSON.stringify(projectCurrentProgress(status))}\n`);
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
const endpointPath = join(stateDir, "playback-endpoint.json");
|
|
126
|
-
if (!existsSync(endpointPath)) fail(`playback service is not active: ${stateDir}`, 3);
|
|
127
|
-
const endpoint = readJson(endpointPath, {});
|
|
128
|
-
if (
|
|
129
|
-
typeof endpoint.path !== "string" ||
|
|
130
|
-
!endpoint.path ||
|
|
131
|
-
typeof endpoint.service_instance_id !== "string" ||
|
|
132
|
-
!endpoint.service_instance_id
|
|
133
|
-
) fail(`playback service endpoint is invalid: ${stateDir}`, 3);
|
|
134
|
-
let input;
|
|
135
|
-
if (action === "seek" || action === "volume") {
|
|
136
|
-
input = { percent: parsePercent(actionArgs[0], action) };
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
await sendCommand(endpoint, action === "resume" ? "play" : action, input);
|
|
140
|
-
console.log(`music-player: command=${action} handled state_dir=${stateDir}`);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
fail(error instanceof Error ? error.message : String(error), 3);
|
|
143
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Actor-neutral client for the local Music Player playback protocol.
|
|
5
|
-
*
|
|
6
|
-
* Reads portable JSON status and sends bounded generation-fenced commands to
|
|
7
|
-
* the playback service endpoint without reading pi-actors Run or Control state.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
-
import { createConnection } from "node:net";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
const ACTIONS = new Set([
|
|
16
|
-
"play",
|
|
17
|
-
"resume",
|
|
18
|
-
"pause",
|
|
19
|
-
"toggle",
|
|
20
|
-
"next",
|
|
21
|
-
"previous",
|
|
22
|
-
"seek",
|
|
23
|
-
"volume",
|
|
24
|
-
"stop",
|
|
25
|
-
"status",
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
function fail(message, code = 1) {
|
|
29
|
-
console.error(`music-player: ${message}`);
|
|
30
|
-
process.exit(code);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function expandPath(value) {
|
|
34
|
-
if (value === "~") return homedir();
|
|
35
|
-
if (value.startsWith("~/")) return join(homedir(), value.slice(2));
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function readJson(path, fallback) {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
42
|
-
} catch {
|
|
43
|
-
return fallback;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parsePercent(value, label) {
|
|
48
|
-
if (!/^\d+$/.test(String(value))) {
|
|
49
|
-
fail(`${label} percent must be an integer 0..100`, 2);
|
|
50
|
-
}
|
|
51
|
-
const percent = Number(value);
|
|
52
|
-
if (percent < 0 || percent > 100) {
|
|
53
|
-
fail(`${label} percent must be an integer 0..100`, 2);
|
|
54
|
-
}
|
|
55
|
-
return percent;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function projectCurrentProgress(status, nowMs = Date.now()) {
|
|
59
|
-
const duration = Number(status.duration_seconds);
|
|
60
|
-
let position = Number(status.position_seconds);
|
|
61
|
-
const updatedAtMs = Number(status.position_updated_at_ms);
|
|
62
|
-
if (!Number.isFinite(position)) position = 0;
|
|
63
|
-
if (status.state === "playing" && Number.isFinite(updatedAtMs)) {
|
|
64
|
-
position += Math.max(0, nowMs - updatedAtMs) / 1000;
|
|
65
|
-
}
|
|
66
|
-
if (!Number.isFinite(duration) || duration <= 0) {
|
|
67
|
-
return { ...status, progress_percent: null };
|
|
68
|
-
}
|
|
69
|
-
position = Math.min(Math.max(position, 0), duration);
|
|
70
|
-
return {
|
|
71
|
-
...status,
|
|
72
|
-
progress_percent: Math.round((position / duration) * 100),
|
|
73
|
-
position_seconds: position,
|
|
74
|
-
position_updated_at_ms: nowMs,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function sendCommand(endpoint, action, input) {
|
|
79
|
-
const payload = `${JSON.stringify({
|
|
80
|
-
action,
|
|
81
|
-
...(input !== undefined ? { input } : {}),
|
|
82
|
-
service_instance_id: endpoint.service_instance_id,
|
|
83
|
-
})}\n`;
|
|
84
|
-
const response = await new Promise((resolveResponse, rejectResponse) => {
|
|
85
|
-
const socket = createConnection(endpoint.path);
|
|
86
|
-
let content = "";
|
|
87
|
-
const timeout = setTimeout(() => {
|
|
88
|
-
socket.destroy();
|
|
89
|
-
rejectResponse(new Error("playback service command timed out"));
|
|
90
|
-
}, 5_000);
|
|
91
|
-
timeout.unref?.();
|
|
92
|
-
socket.setEncoding("utf8");
|
|
93
|
-
socket.on("connect", () => socket.write(payload));
|
|
94
|
-
socket.on("data", (chunk) => {
|
|
95
|
-
content += chunk;
|
|
96
|
-
if (Buffer.byteLength(content, "utf8") > 4096) {
|
|
97
|
-
socket.destroy(new Error("playback service response is too large"));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
socket.on("error", rejectResponse);
|
|
101
|
-
socket.on("end", () => {
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
try {
|
|
104
|
-
resolveResponse(JSON.parse(content.trim()));
|
|
105
|
-
} catch {
|
|
106
|
-
rejectResponse(new Error("playback service returned invalid JSON"));
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
if (!response?.ok) {
|
|
111
|
-
throw new Error(response?.error || "playback service rejected the command");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const [action = "status", rawStateDir, ...actionArgs] = process.argv.slice(2);
|
|
116
|
-
if (!ACTIONS.has(action) || !rawStateDir) {
|
|
117
|
-
fail("usage: playback-client.mjs <status|play|resume|pause|toggle|next|previous|seek|volume|stop> <state-dir> [action args]", 2);
|
|
118
|
-
}
|
|
119
|
-
const stateDir = expandPath(rawStateDir);
|
|
120
|
-
if (action === "status") {
|
|
121
|
-
const status = readJson(join(stateDir, "player.json"), { state: "unknown" });
|
|
122
|
-
process.stdout.write(`${JSON.stringify(projectCurrentProgress(status))}\n`);
|
|
123
|
-
process.exit(0);
|
|
124
|
-
}
|
|
125
|
-
const endpointPath = join(stateDir, "playback-endpoint.json");
|
|
126
|
-
if (!existsSync(endpointPath)) fail(`playback service is not active: ${stateDir}`, 3);
|
|
127
|
-
const endpoint = readJson(endpointPath, {});
|
|
128
|
-
if (
|
|
129
|
-
typeof endpoint.path !== "string" ||
|
|
130
|
-
!endpoint.path ||
|
|
131
|
-
typeof endpoint.service_instance_id !== "string" ||
|
|
132
|
-
!endpoint.service_instance_id
|
|
133
|
-
) fail(`playback service endpoint is invalid: ${stateDir}`, 3);
|
|
134
|
-
let input;
|
|
135
|
-
if (action === "seek" || action === "volume") {
|
|
136
|
-
input = { percent: parsePercent(actionArgs[0], action) };
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
await sendCommand(endpoint, action === "resume" ? "play" : action, input);
|
|
140
|
-
console.log(`music-player: command=${action} handled state_dir=${stateDir}`);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
fail(error instanceof Error ? error.message : String(error), 3);
|
|
143
|
-
}
|