@drewpayment/mink 0.3.0 → 0.5.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/README.md +235 -11
- package/dist/cli.js +1424 -487
- package/package.json +1 -1
- package/src/cli.ts +23 -1
- package/src/commands/channel.ts +252 -0
- package/src/commands/config.ts +1 -1
- package/src/commands/device.ts +65 -0
- package/src/commands/session-start.ts +16 -0
- package/src/core/channel-process.ts +274 -0
- package/src/core/channel-templates.ts +156 -0
- package/src/core/daemon.ts +63 -2
- package/src/core/device.ts +72 -0
- package/src/core/global-config.ts +72 -11
- package/src/core/paths.ts +20 -0
- package/src/core/sync.ts +12 -0
- package/src/types/channel.ts +16 -0
- package/src/types/config.ts +57 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
|
|
4
|
+
export const COMPANION_CLAUDE_MD = `# Mink Knowledge Companion
|
|
5
|
+
|
|
6
|
+
You are **Mink**, a personal knowledge companion. You help capture, organize, search, and retrieve notes across all the user's projects through conversational messages (Discord, Telegram, or iMessage via Claude Code Channels).
|
|
7
|
+
|
|
8
|
+
Your home is this wiki vault — the directory you're running in. Notes live as markdown files organized by PARA (Projects / Areas / Resources / Archives / Inbox).
|
|
9
|
+
|
|
10
|
+
## Your Role
|
|
11
|
+
|
|
12
|
+
You are the **smart orchestrator**. The \`mink\` CLI is the dumb writer — it takes explicit flags and writes files. Your job:
|
|
13
|
+
|
|
14
|
+
1. Understand what the user wants (capture, search, organize, summarize)
|
|
15
|
+
2. Gather vault context when useful
|
|
16
|
+
3. Call the right \`mink\` command with good flags
|
|
17
|
+
4. Reply briefly — the user is likely on mobile
|
|
18
|
+
|
|
19
|
+
## Conversational Style
|
|
20
|
+
|
|
21
|
+
- **Brief.** One or two sentences. The user is in a chat app, not a terminal.
|
|
22
|
+
- **Confirm what happened.** "Saved to \`projects/auth/blocker.md\` with tags \`compliance, blocker\`." — short, specific.
|
|
23
|
+
- **Suggest, don't interrogate.** If you're unsure about a tag, pick a reasonable default and mention it. Don't ask 3 questions before saving a note.
|
|
24
|
+
- **Surface related work.** After saving, mention related notes found ("2 related notes about auth-migration") when useful.
|
|
25
|
+
|
|
26
|
+
## Capturing Notes
|
|
27
|
+
|
|
28
|
+
When the user's message sounds like a note ("save this...", "log that...", or just describes something factual), **capture it**. Don't ask permission.
|
|
29
|
+
|
|
30
|
+
### Flow
|
|
31
|
+
|
|
32
|
+
1. **Read the message.** Extract: what's this about? Is it project-specific?
|
|
33
|
+
2. **Gather context briefly.** Run these when needed (not every time):
|
|
34
|
+
- \`mink note list --recent 10\` — recent notes for continuity
|
|
35
|
+
- \`mink wiki status\` — vault overview
|
|
36
|
+
- Check \`.mink-index.json\` for existing tag vocabulary
|
|
37
|
+
3. **Decide metadata:**
|
|
38
|
+
- **Title** — clear, descriptive (becomes the filename). Not the raw text.
|
|
39
|
+
- **Category** — one of:
|
|
40
|
+
- \`projects\` — has a deadline, milestone, or deliverable. Use \`--project <slug>\` if it maps to a known Mink project.
|
|
41
|
+
- \`areas\` — ongoing responsibility or recurring concern
|
|
42
|
+
- \`resources\` — reference material, how-to, guide
|
|
43
|
+
- \`archives\` — completed or historical
|
|
44
|
+
- \`inbox\` — genuinely unclear (user will sort later)
|
|
45
|
+
- **Tags** — 2–3 is usually right. **Prefer existing tags** from the vocabulary over inventing new ones. Lowercase, hyphenated.
|
|
46
|
+
- **Wikilinks** — wrap people, projects, and concepts in \`[[double-brackets]]\` inside the body when they match existing notes.
|
|
47
|
+
4. **Call \`mink note\`** with the flags:
|
|
48
|
+
\`\`\`bash
|
|
49
|
+
mink note --title "Title" --body "Body with [[wikilinks]]" --category <cat> --tags "a,b,c"
|
|
50
|
+
# Add --project <slug> if project-linked
|
|
51
|
+
\`\`\`
|
|
52
|
+
5. **Reply.** One line: where it landed, category, tags. Optionally: related notes.
|
|
53
|
+
|
|
54
|
+
### Daily Notes
|
|
55
|
+
|
|
56
|
+
If the user says "add to my daily" or "daily" or "today":
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
mink note --daily "Content to append"
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
### Meeting Notes
|
|
62
|
+
|
|
63
|
+
If the user describes a meeting (attendees, topic, discussion):
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
mink note --template meeting --title "Meeting: Topic" --body "..." --category areas --tags "meeting,..."
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
## Searching and Retrieving
|
|
69
|
+
|
|
70
|
+
When the user asks about past work — "what did I write about X?", "show me notes from last week", "find my notes on auth" — use:
|
|
71
|
+
|
|
72
|
+
- \`mink note search <term>\` — full-text search (title, description, tags, path)
|
|
73
|
+
- \`mink note list --recent N\` — recent notes
|
|
74
|
+
- \`mink note list --category projects\` — filter by category
|
|
75
|
+
- \`mink note list --tag auth\` — filter by tag
|
|
76
|
+
|
|
77
|
+
**Return results briefly.** Top 3–5 matches with one-line summaries. If there are more, say so.
|
|
78
|
+
|
|
79
|
+
Example reply:
|
|
80
|
+
> Found 3 notes about auth-migration:
|
|
81
|
+
> • \`projects/auth/compliance-blocker.md\` (today) — blocked on session token storage
|
|
82
|
+
> • \`projects/auth/architecture.md\` (Apr 10) — middleware rewrite plan
|
|
83
|
+
> • \`areas/daily/2026-04-12.md\` — standup update
|
|
84
|
+
|
|
85
|
+
## Organization
|
|
86
|
+
|
|
87
|
+
If the user says "move this to projects", "tag this with X", or "categorize my inbox":
|
|
88
|
+
|
|
89
|
+
- For a single note: read it, rewrite it in the new category with \`mink note --file\` (ingestion). The CLI doesn't have a move command — you move by rewriting.
|
|
90
|
+
- For inbox triage: list with \`mink note list --category inbox\`, propose categorization, execute on confirmation.
|
|
91
|
+
|
|
92
|
+
## Daily Summaries
|
|
93
|
+
|
|
94
|
+
If the user asks "what did I work on today?" or "give me a summary":
|
|
95
|
+
|
|
96
|
+
1. Read today's daily note: \`mink note list --tag daily --recent 1\` → read the file
|
|
97
|
+
2. Check recent notes: \`mink note list --recent 20\`
|
|
98
|
+
3. Synthesize a short summary (3–5 bullets)
|
|
99
|
+
|
|
100
|
+
## Cross-Project Awareness
|
|
101
|
+
|
|
102
|
+
Notes may be linked to projects via \`source_project\` in frontmatter. To find notes for a specific project:
|
|
103
|
+
\`\`\`bash
|
|
104
|
+
mink note list --category projects
|
|
105
|
+
mink note search <project-slug>
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
Use wikilinks generously: \`[[project-name]]\`, \`[[person-name]]\`, \`[[concept]]\`. If the target note doesn't exist, the wikilink still works as a placeholder.
|
|
109
|
+
|
|
110
|
+
## Session Kickoff
|
|
111
|
+
|
|
112
|
+
At the start of a fresh conversation (first user message), it's fine to silently run:
|
|
113
|
+
\`\`\`bash
|
|
114
|
+
mink wiki status
|
|
115
|
+
mink note list --recent 5
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
Don't announce this. Just have the context.
|
|
119
|
+
|
|
120
|
+
## What NOT to Do
|
|
121
|
+
|
|
122
|
+
- Don't ask "what category should this be?" — pick one, move on.
|
|
123
|
+
- Don't paste long output. Summarize.
|
|
124
|
+
- Don't invent tags that exist with slight variations. Check vocabulary first.
|
|
125
|
+
- Don't open files or directories unrelated to the vault. Stay focused on notes and wiki operations.
|
|
126
|
+
- Don't edit source code in this vault — this is a knowledge repo, not a codebase.
|
|
127
|
+
|
|
128
|
+
## CLI Reference (Cheat Sheet)
|
|
129
|
+
|
|
130
|
+
\`\`\`bash
|
|
131
|
+
# Capture
|
|
132
|
+
mink note "quick thought" # inbox capture
|
|
133
|
+
mink note --title T --body B --category areas --tags a,b
|
|
134
|
+
mink note --daily "content" # daily note
|
|
135
|
+
mink note --template meeting --title "..." --body "..."
|
|
136
|
+
mink note --file ./external.md --category resources
|
|
137
|
+
|
|
138
|
+
# Search / list
|
|
139
|
+
mink note search <term>
|
|
140
|
+
mink note list [--category X] [--tag Y] [--recent N]
|
|
141
|
+
|
|
142
|
+
# Vault
|
|
143
|
+
mink wiki status
|
|
144
|
+
mink wiki rebuild-index
|
|
145
|
+
\`\`\`
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
export function writeCompanionClaudeMd(vaultPath: string, overwrite = false): boolean {
|
|
149
|
+
mkdirSync(vaultPath, { recursive: true });
|
|
150
|
+
const claudeMdPath = join(vaultPath, "CLAUDE.md");
|
|
151
|
+
if (existsSync(claudeMdPath) && !overwrite) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
writeFileSync(claudeMdPath, COMPANION_CLAUDE_MD);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -3,6 +3,16 @@ import { mkdirSync } from "fs";
|
|
|
3
3
|
import { dirname, resolve } from "path";
|
|
4
4
|
import { schedulerPidPath, schedulerLogPath } from "./paths";
|
|
5
5
|
import type { PidFileData } from "../types/scheduler";
|
|
6
|
+
import { resolveConfigValue } from "./global-config";
|
|
7
|
+
import { resolveVaultPath, isVaultInitialized } from "./vault";
|
|
8
|
+
import { writeCompanionClaudeMd } from "./channel-templates";
|
|
9
|
+
import {
|
|
10
|
+
startChannelProcess,
|
|
11
|
+
stopChannelProcess,
|
|
12
|
+
getChannelStatus,
|
|
13
|
+
isChannelRunning,
|
|
14
|
+
} from "./channel-process";
|
|
15
|
+
import type { ChannelPlatform, ChannelStatus } from "../types/channel";
|
|
6
16
|
|
|
7
17
|
// ── PID File Operations ─────────────────────────────────────────────────────
|
|
8
18
|
|
|
@@ -91,9 +101,49 @@ export function startDaemon(cwd: string): void {
|
|
|
91
101
|
|
|
92
102
|
console.log(`[mink] scheduler started (PID: ${proc.pid})`);
|
|
93
103
|
console.log(`[mink] log: ${logPath}`);
|
|
104
|
+
|
|
105
|
+
// Fire and forget — channel startup has its own verification loop.
|
|
106
|
+
maybeStartChannel().catch((err) => {
|
|
107
|
+
console.error(`[mink] failed to start channel: ${err instanceof Error ? err.message : String(err)}`);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function maybeStartChannel(): Promise<void> {
|
|
112
|
+
const enabled = resolveConfigValue("channel.discord.enabled").value === "true";
|
|
113
|
+
if (!enabled) return;
|
|
114
|
+
|
|
115
|
+
if (!isVaultInitialized()) {
|
|
116
|
+
console.log("[mink] channel enabled but vault not initialized — skipping channel start");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const token = resolveConfigValue("channel.discord.bot-token").value;
|
|
121
|
+
if (!token) {
|
|
122
|
+
console.log("[mink] channel enabled but no Discord bot token configured — skipping channel start");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isChannelRunning()) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const platform =
|
|
131
|
+
(resolveConfigValue("channel.default-platform").value as ChannelPlatform) ||
|
|
132
|
+
"discord";
|
|
133
|
+
const skipPermissions =
|
|
134
|
+
resolveConfigValue("channel.skip-permissions").value === "true";
|
|
135
|
+
const vaultPath = resolveVaultPath();
|
|
136
|
+
writeCompanionClaudeMd(vaultPath, false);
|
|
137
|
+
|
|
138
|
+
const result = await startChannelProcess({ vaultPath, platform, token, skipPermissions });
|
|
139
|
+
if (!result.alreadyRunning) {
|
|
140
|
+
console.log(`[mink] channel started (session: ${result.session}, platform: ${platform})`);
|
|
141
|
+
}
|
|
94
142
|
}
|
|
95
143
|
|
|
96
144
|
export async function stopDaemon(): Promise<void> {
|
|
145
|
+
await stopChannelIfRunning();
|
|
146
|
+
|
|
97
147
|
const pidData = readPidFile();
|
|
98
148
|
if (!pidData) {
|
|
99
149
|
console.log("[mink] scheduler is not running (no PID file)");
|
|
@@ -130,24 +180,35 @@ export async function stopDaemon(): Promise<void> {
|
|
|
130
180
|
console.log("[mink] scheduler force-stopped (SIGKILL)");
|
|
131
181
|
}
|
|
132
182
|
|
|
183
|
+
async function stopChannelIfRunning(): Promise<void> {
|
|
184
|
+
if (!isChannelRunning()) return;
|
|
185
|
+
const result = await stopChannelProcess();
|
|
186
|
+
if (result === "stopped") {
|
|
187
|
+
console.log("[mink] channel stopped");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
133
191
|
export function getDaemonStatus(cwd: string): {
|
|
134
192
|
running: boolean;
|
|
135
193
|
pid?: number;
|
|
136
194
|
startedAt?: string;
|
|
137
195
|
projectCwd?: string;
|
|
196
|
+
channel: ChannelStatus | null;
|
|
138
197
|
} {
|
|
198
|
+
const channel = getChannelStatus();
|
|
139
199
|
const pidData = readPidFile();
|
|
140
200
|
if (!pidData) {
|
|
141
|
-
return { running: false };
|
|
201
|
+
return { running: false, channel };
|
|
142
202
|
}
|
|
143
203
|
if (!isProcessAlive(pidData.pid)) {
|
|
144
204
|
removePidFile();
|
|
145
|
-
return { running: false };
|
|
205
|
+
return { running: false, channel };
|
|
146
206
|
}
|
|
147
207
|
return {
|
|
148
208
|
running: true,
|
|
149
209
|
pid: pidData.pid,
|
|
150
210
|
startedAt: pidData.startedAt,
|
|
151
211
|
projectCwd: pidData.projectCwd,
|
|
212
|
+
channel,
|
|
152
213
|
};
|
|
153
214
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import { hostname, platform } from "os";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { deviceIdPath, deviceRegistryPath } from "./paths";
|
|
6
|
+
import { safeReadJson, atomicWriteJson } from "./fs-utils";
|
|
7
|
+
import type { DeviceInfo, DeviceRegistry } from "../types/config";
|
|
8
|
+
|
|
9
|
+
export function getOrCreateDeviceId(): string {
|
|
10
|
+
const idPath = deviceIdPath();
|
|
11
|
+
if (existsSync(idPath)) {
|
|
12
|
+
return readFileSync(idPath, "utf-8").trim();
|
|
13
|
+
}
|
|
14
|
+
const id = randomUUID();
|
|
15
|
+
mkdirSync(dirname(idPath), { recursive: true });
|
|
16
|
+
writeFileSync(idPath, id + "\n");
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadDeviceRegistry(): DeviceRegistry {
|
|
21
|
+
const raw = safeReadJson(deviceRegistryPath());
|
|
22
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw) && "devices" in (raw as object)) {
|
|
23
|
+
return raw as DeviceRegistry;
|
|
24
|
+
}
|
|
25
|
+
return { devices: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveDeviceRegistry(registry: DeviceRegistry): void {
|
|
29
|
+
atomicWriteJson(deviceRegistryPath(), registry);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function updateDeviceHeartbeat(): void {
|
|
33
|
+
const id = getOrCreateDeviceId();
|
|
34
|
+
const registry = loadDeviceRegistry();
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const existing = registry.devices[id];
|
|
37
|
+
|
|
38
|
+
registry.devices[id] = {
|
|
39
|
+
name: existing?.name ?? hostname(),
|
|
40
|
+
hostname: hostname(),
|
|
41
|
+
platform: platform(),
|
|
42
|
+
firstSeen: existing?.firstSeen ?? now,
|
|
43
|
+
lastSeen: now,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
saveDeviceRegistry(registry);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function listDevices(): Array<DeviceInfo & { id: string }> {
|
|
50
|
+
const registry = loadDeviceRegistry();
|
|
51
|
+
return Object.entries(registry.devices).map(([id, info]) => ({
|
|
52
|
+
id,
|
|
53
|
+
...info,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setDeviceName(name: string): void {
|
|
58
|
+
const id = getOrCreateDeviceId();
|
|
59
|
+
const registry = loadDeviceRegistry();
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const existing = registry.devices[id];
|
|
62
|
+
|
|
63
|
+
registry.devices[id] = {
|
|
64
|
+
name,
|
|
65
|
+
hostname: hostname(),
|
|
66
|
+
platform: platform(),
|
|
67
|
+
firstSeen: existing?.firstSeen ?? now,
|
|
68
|
+
lastSeen: now,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
saveDeviceRegistry(registry);
|
|
72
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { globalConfigPath } from "./paths";
|
|
1
|
+
import { globalConfigPath, localConfigPath } from "./paths";
|
|
2
2
|
import { safeReadJson, atomicWriteJson } from "./fs-utils";
|
|
3
3
|
import {
|
|
4
4
|
CONFIG_KEYS,
|
|
@@ -6,31 +6,57 @@ import {
|
|
|
6
6
|
getConfigKeyMeta,
|
|
7
7
|
type GlobalConfig,
|
|
8
8
|
type ConfigKey,
|
|
9
|
+
type ConfigScope,
|
|
9
10
|
} from "../types/config";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
const raw = safeReadJson(
|
|
12
|
+
function loadConfigFile(path: string): GlobalConfig {
|
|
13
|
+
const raw = safeReadJson(path);
|
|
13
14
|
if (raw === null) return {};
|
|
14
15
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
15
|
-
console.warn("[mink] warning: corrupt config file at " +
|
|
16
|
+
console.warn("[mink] warning: corrupt config file at " + path);
|
|
16
17
|
return {};
|
|
17
18
|
}
|
|
18
19
|
return raw as GlobalConfig;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function loadGlobalConfig(): GlobalConfig {
|
|
23
|
+
return loadConfigFile(globalConfigPath());
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function saveGlobalConfig(config: GlobalConfig): void {
|
|
22
27
|
atomicWriteJson(globalConfigPath(), config);
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
export function loadLocalConfig(): GlobalConfig {
|
|
31
|
+
return loadConfigFile(localConfigPath());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function saveLocalConfig(config: GlobalConfig): void {
|
|
35
|
+
atomicWriteJson(localConfigPath(), config);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadConfigForScope(scope: ConfigScope): GlobalConfig {
|
|
39
|
+
return scope === "local" ? loadLocalConfig() : loadGlobalConfig();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveConfigForScope(scope: ConfigScope, config: GlobalConfig): void {
|
|
43
|
+
if (scope === "local") {
|
|
44
|
+
saveLocalConfig(config);
|
|
45
|
+
} else {
|
|
46
|
+
saveGlobalConfig(config);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
export interface ResolvedValue {
|
|
26
51
|
value: string;
|
|
27
52
|
source: "default" | "config file" | "environment variable";
|
|
53
|
+
scope: ConfigScope;
|
|
28
54
|
configFileValue?: string;
|
|
29
55
|
}
|
|
30
56
|
|
|
31
57
|
export function resolveConfigValue(key: ConfigKey): ResolvedValue {
|
|
32
58
|
const meta = getConfigKeyMeta(key);
|
|
33
|
-
const config =
|
|
59
|
+
const config = loadConfigForScope(meta.scope);
|
|
34
60
|
|
|
35
61
|
const envValue = process.env[meta.envVar];
|
|
36
62
|
const fileValue = config[key];
|
|
@@ -39,15 +65,16 @@ export function resolveConfigValue(key: ConfigKey): ResolvedValue {
|
|
|
39
65
|
return {
|
|
40
66
|
value: envValue,
|
|
41
67
|
source: "environment variable",
|
|
68
|
+
scope: meta.scope,
|
|
42
69
|
configFileValue: fileValue,
|
|
43
70
|
};
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
if (fileValue !== undefined) {
|
|
47
|
-
return { value: fileValue, source: "config file" };
|
|
74
|
+
return { value: fileValue, source: "config file", scope: meta.scope };
|
|
48
75
|
}
|
|
49
76
|
|
|
50
|
-
return { value: meta.default, source: "default" };
|
|
77
|
+
return { value: meta.default, source: "default", scope: meta.scope };
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
export function resolveAllConfig(): Array<ResolvedValue & { key: ConfigKey }> {
|
|
@@ -58,17 +85,51 @@ export function resolveAllConfig(): Array<ResolvedValue & { key: ConfigKey }> {
|
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
export function setConfigValue(key: ConfigKey, value: string): void {
|
|
61
|
-
const
|
|
88
|
+
const meta = getConfigKeyMeta(key);
|
|
89
|
+
const config = loadConfigForScope(meta.scope);
|
|
62
90
|
config[key] = value;
|
|
63
|
-
|
|
91
|
+
saveConfigForScope(meta.scope, config);
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
export function resetConfigKey(key: ConfigKey): void {
|
|
67
|
-
const
|
|
95
|
+
const meta = getConfigKeyMeta(key);
|
|
96
|
+
const config = loadConfigForScope(meta.scope);
|
|
68
97
|
delete config[key];
|
|
69
|
-
|
|
98
|
+
saveConfigForScope(meta.scope, config);
|
|
70
99
|
}
|
|
71
100
|
|
|
72
101
|
export function resetAllConfig(): void {
|
|
73
102
|
saveGlobalConfig({});
|
|
103
|
+
saveLocalConfig({});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Migration ─────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
let migrationRan = false;
|
|
109
|
+
|
|
110
|
+
export function migrateConfigIfNeeded(): void {
|
|
111
|
+
if (migrationRan) return;
|
|
112
|
+
migrationRan = true;
|
|
113
|
+
|
|
114
|
+
const { existsSync } = require("fs");
|
|
115
|
+
if (existsSync(localConfigPath())) return;
|
|
116
|
+
|
|
117
|
+
const shared = loadGlobalConfig();
|
|
118
|
+
const localKeys = CONFIG_KEYS.filter((k) => k.scope === "local");
|
|
119
|
+
const localConfig: GlobalConfig = {};
|
|
120
|
+
let hasLocal = false;
|
|
121
|
+
|
|
122
|
+
for (const meta of localKeys) {
|
|
123
|
+
const val = shared[meta.key];
|
|
124
|
+
if (val !== undefined) {
|
|
125
|
+
localConfig[meta.key] = val;
|
|
126
|
+
delete shared[meta.key];
|
|
127
|
+
hasLocal = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (hasLocal) {
|
|
132
|
+
saveLocalConfig(localConfig);
|
|
133
|
+
saveGlobalConfig(shared);
|
|
134
|
+
}
|
|
74
135
|
}
|
package/src/core/paths.ts
CHANGED
|
@@ -57,10 +57,30 @@ export function schedulerManifestPath(cwd: string): string {
|
|
|
57
57
|
return join(projectDir(cwd), "scheduler-manifest.json");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
export function channelPidPath(): string {
|
|
61
|
+
return join(MINK_ROOT, "channel.pid");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function channelLogPath(): string {
|
|
65
|
+
return join(MINK_ROOT, "channel.log");
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
export function globalConfigPath(): string {
|
|
61
69
|
return join(MINK_ROOT, "config");
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
export function localConfigPath(): string {
|
|
73
|
+
return join(MINK_ROOT, "config.local");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function deviceIdPath(): string {
|
|
77
|
+
return join(MINK_ROOT, "device-id");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function deviceRegistryPath(): string {
|
|
81
|
+
return join(MINK_ROOT, "devices.json");
|
|
82
|
+
}
|
|
83
|
+
|
|
64
84
|
export function projectMetaPath(cwd: string): string {
|
|
65
85
|
return join(projectDir(cwd), "project-meta.json");
|
|
66
86
|
}
|
package/src/core/sync.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "path";
|
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
4
|
import { minkRoot } from "./paths";
|
|
5
5
|
import { resolveConfigValue, setConfigValue } from "./global-config";
|
|
6
|
+
import { updateDeviceHeartbeat } from "./device";
|
|
6
7
|
|
|
7
8
|
// ── Constants ──────────────────────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -14,6 +15,10 @@ const GITIGNORE_CONTENTS = `# Runtime state — machine-specific
|
|
|
14
15
|
scheduler.pid
|
|
15
16
|
scheduler.log
|
|
16
17
|
|
|
18
|
+
# Device identity and local config — machine-specific
|
|
19
|
+
device-id
|
|
20
|
+
config.local
|
|
21
|
+
|
|
17
22
|
# Local backups — machine-specific snapshots
|
|
18
23
|
projects/*/backups/
|
|
19
24
|
`;
|
|
@@ -171,6 +176,8 @@ export function syncPull(
|
|
|
171
176
|
): void {
|
|
172
177
|
if (!isSyncInitialized()) return;
|
|
173
178
|
|
|
179
|
+
ensureGitignore();
|
|
180
|
+
|
|
174
181
|
const root = minkRoot();
|
|
175
182
|
|
|
176
183
|
try {
|
|
@@ -218,6 +225,8 @@ export function syncPull(
|
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
setConfigValue("sync.last-pull", new Date().toISOString());
|
|
228
|
+
|
|
229
|
+
try { updateDeviceHeartbeat(); } catch { /* never crash hooks */ }
|
|
221
230
|
} catch (err) {
|
|
222
231
|
onMessage(
|
|
223
232
|
`[mink] sync pull error: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -230,6 +239,9 @@ export function syncPush(
|
|
|
230
239
|
): void {
|
|
231
240
|
if (!isSyncInitialized()) return;
|
|
232
241
|
|
|
242
|
+
ensureGitignore();
|
|
243
|
+
try { updateDeviceHeartbeat(); } catch { /* never crash hooks */ }
|
|
244
|
+
|
|
233
245
|
const root = minkRoot();
|
|
234
246
|
|
|
235
247
|
try {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ChannelPlatform = "discord" | "telegram";
|
|
2
|
+
|
|
3
|
+
export interface ChannelPidFile {
|
|
4
|
+
session: string;
|
|
5
|
+
platform: ChannelPlatform;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
vaultPath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ChannelStatus {
|
|
11
|
+
session: string;
|
|
12
|
+
platform: ChannelPlatform;
|
|
13
|
+
startedAt: string;
|
|
14
|
+
vaultPath: string;
|
|
15
|
+
uptime: number;
|
|
16
|
+
}
|