selftune 0.2.18 → 0.2.20
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 +9 -4
- package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +60 -0
- package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +1 -0
- package/apps/local-dashboard/dist/assets/vendor-table-BIiI3YhS.js +1 -0
- package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +12 -0
- package/apps/local-dashboard/dist/index.html +5 -5
- package/cli/selftune/alpha-upload/stage-canonical.ts +7 -6
- package/cli/selftune/constants.ts +10 -0
- package/cli/selftune/contribute/contribute.ts +30 -2
- package/cli/selftune/contribution-config.ts +249 -0
- package/cli/selftune/contribution-relay.ts +177 -0
- package/cli/selftune/contribution-signals.ts +219 -0
- package/cli/selftune/contribution-staging.ts +147 -0
- package/cli/selftune/contributions.ts +532 -0
- package/cli/selftune/creator-contributions.ts +333 -0
- package/cli/selftune/dashboard-contract.ts +209 -1
- package/cli/selftune/dashboard-server.ts +45 -11
- package/cli/selftune/eval/family-overlap.ts +714 -0
- package/cli/selftune/eval/hooks-to-evals.ts +182 -28
- package/cli/selftune/eval/synthetic-evals.ts +298 -11
- package/cli/selftune/evolution/evidence.ts +5 -0
- package/cli/selftune/evolution/evolve-body.ts +62 -2
- package/cli/selftune/evolution/evolve.ts +58 -1
- package/cli/selftune/evolution/validate-body.ts +10 -0
- package/cli/selftune/evolution/validate-host-replay.ts +236 -0
- package/cli/selftune/evolution/validate-proposal.ts +10 -0
- package/cli/selftune/evolution/validate-routing.ts +112 -5
- package/cli/selftune/export.ts +2 -2
- package/cli/selftune/index.ts +41 -5
- package/cli/selftune/ingestors/codex-rollout.ts +31 -35
- package/cli/selftune/ingestors/codex-wrapper.ts +32 -24
- package/cli/selftune/localdb/db.ts +2 -2
- package/cli/selftune/localdb/direct-write.ts +8 -3
- package/cli/selftune/localdb/materialize.ts +7 -2
- package/cli/selftune/localdb/queries.ts +712 -31
- package/cli/selftune/localdb/schema.ts +30 -1
- package/cli/selftune/recover.ts +153 -0
- package/cli/selftune/repair/skill-usage.ts +363 -4
- package/cli/selftune/routes/actions.ts +35 -1
- package/cli/selftune/routes/analytics.ts +14 -0
- package/cli/selftune/routes/index.ts +1 -0
- package/cli/selftune/routes/overview.ts +112 -4
- package/cli/selftune/routes/skill-report.ts +575 -11
- package/cli/selftune/status.ts +81 -2
- package/cli/selftune/sync.ts +56 -2
- package/cli/selftune/trust-model.ts +66 -0
- package/cli/selftune/types.ts +103 -0
- package/cli/selftune/utils/skill-detection.ts +43 -0
- package/cli/selftune/utils/text-similarity.ts +73 -0
- package/cli/selftune/watchlist.ts +65 -0
- package/package.json +1 -1
- package/packages/ui/src/components/ActivityTimeline.tsx +165 -150
- package/packages/ui/src/components/EvidenceViewer.tsx +419 -145
- package/packages/ui/src/components/EvolutionTimeline.tsx +81 -29
- package/packages/ui/src/components/OrchestrateRunsPanel.tsx +33 -16
- package/packages/ui/src/components/RecentActivityFeed.tsx +72 -41
- package/packages/ui/src/components/section-cards.tsx +12 -9
- package/packages/ui/src/primitives/card.tsx +1 -1
- package/packages/ui/src/types.ts +4 -0
- package/skill/SKILL.md +11 -1
- package/skill/Workflows/AlphaUpload.md +4 -0
- package/skill/Workflows/Composability.md +78 -0
- package/skill/Workflows/Contribute.md +6 -3
- package/skill/Workflows/Contributions.md +97 -0
- package/skill/Workflows/CreatorContributions.md +74 -0
- package/skill/Workflows/Dashboard.md +31 -0
- package/skill/Workflows/Evals.md +57 -8
- package/skill/Workflows/Evolve.md +23 -0
- package/skill/Workflows/Ingest.md +7 -0
- package/skill/Workflows/Initialize.md +20 -1
- package/skill/Workflows/Recover.md +84 -0
- package/skill/Workflows/RepairSkillUsage.md +12 -4
- package/skill/Workflows/Sync.md +18 -12
- package/apps/local-dashboard/dist/assets/index-BMIS6uUh.css +0 -2
- package/apps/local-dashboard/dist/assets/index-DOu3iLD9.js +0 -16
- package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +0 -8
- package/apps/local-dashboard/dist/assets/vendor-ui-DIwlrGlb.js +0 -12
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
import type { CreatorContributionConfig } from "./contribution-config.js";
|
|
4
|
+
import { discoverCreatorContributionConfigs } from "./contribution-config.js";
|
|
5
|
+
import { buildCreatorDirectedContributionSignals } from "./contribution-signals.js";
|
|
6
|
+
import { loadContributionPreferences, type ContributionPreferences } from "./contributions.js";
|
|
7
|
+
|
|
8
|
+
export interface CreatorContributionStagingResult {
|
|
9
|
+
eligible_skills: number;
|
|
10
|
+
built_signals: number;
|
|
11
|
+
staged_signals: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreatorContributionRelayQueueItem {
|
|
15
|
+
id: number;
|
|
16
|
+
dedupe_key: string;
|
|
17
|
+
skill_name: string;
|
|
18
|
+
creator_id: string;
|
|
19
|
+
payload_json: string;
|
|
20
|
+
status: string;
|
|
21
|
+
staged_at: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
last_error: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CreatorContributionStagingOptions {
|
|
27
|
+
dryRun?: boolean;
|
|
28
|
+
preferences?: ContributionPreferences;
|
|
29
|
+
configs?: CreatorContributionConfig[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveEligibleContributionConfigs(
|
|
33
|
+
preferences: ContributionPreferences = loadContributionPreferences(),
|
|
34
|
+
configs: CreatorContributionConfig[] = discoverCreatorContributionConfigs(),
|
|
35
|
+
): CreatorContributionConfig[] {
|
|
36
|
+
return configs.filter((config) => {
|
|
37
|
+
const pref = preferences.skills[config.skill_name];
|
|
38
|
+
if (pref?.status === "opted_out") return false;
|
|
39
|
+
if (pref?.status === "opted_in") return true;
|
|
40
|
+
return preferences.global_default === "always";
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stageCreatorContributionSignals(
|
|
45
|
+
db: Database,
|
|
46
|
+
options: CreatorContributionStagingOptions = {},
|
|
47
|
+
): CreatorContributionStagingResult {
|
|
48
|
+
const eligibleConfigs = resolveEligibleContributionConfigs(options.preferences, options.configs);
|
|
49
|
+
if (eligibleConfigs.length === 0) {
|
|
50
|
+
return {
|
|
51
|
+
eligible_skills: 0,
|
|
52
|
+
built_signals: 0,
|
|
53
|
+
staged_signals: 0,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const records = buildCreatorDirectedContributionSignals(db, eligibleConfigs);
|
|
58
|
+
if (options.dryRun) {
|
|
59
|
+
return {
|
|
60
|
+
eligible_skills: eligibleConfigs.length,
|
|
61
|
+
built_signals: records.length,
|
|
62
|
+
staged_signals: 0,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const now = new Date().toISOString();
|
|
67
|
+
const stmt = db.prepare(`
|
|
68
|
+
INSERT INTO creator_contribution_staging
|
|
69
|
+
(dedupe_key, skill_name, creator_id, payload_json, status, staged_at, updated_at)
|
|
70
|
+
VALUES (?, ?, ?, ?, 'pending', ?, ?)
|
|
71
|
+
ON CONFLICT(dedupe_key) DO NOTHING
|
|
72
|
+
`);
|
|
73
|
+
|
|
74
|
+
let staged = 0;
|
|
75
|
+
for (const record of records) {
|
|
76
|
+
const result = stmt.run(
|
|
77
|
+
record.source_key,
|
|
78
|
+
record.skill_name,
|
|
79
|
+
record.creator_id,
|
|
80
|
+
JSON.stringify(record.payload),
|
|
81
|
+
now,
|
|
82
|
+
now,
|
|
83
|
+
);
|
|
84
|
+
if (result.changes > 0) staged += 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
eligible_skills: eligibleConfigs.length,
|
|
89
|
+
built_signals: records.length,
|
|
90
|
+
staged_signals: staged,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function markCreatorContributionSending(db: Database, id: number): boolean {
|
|
95
|
+
const now = new Date().toISOString();
|
|
96
|
+
const result = db.run(
|
|
97
|
+
`UPDATE creator_contribution_staging
|
|
98
|
+
SET status = 'sending', updated_at = ?
|
|
99
|
+
WHERE id = ? AND status = 'pending'`,
|
|
100
|
+
[now, id],
|
|
101
|
+
);
|
|
102
|
+
return result.changes > 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function markCreatorContributionSent(db: Database, id: number): boolean {
|
|
106
|
+
const now = new Date().toISOString();
|
|
107
|
+
const result = db.run(
|
|
108
|
+
`UPDATE creator_contribution_staging
|
|
109
|
+
SET status = 'sent', updated_at = ?, last_error = NULL
|
|
110
|
+
WHERE id = ? AND status = 'sending'`,
|
|
111
|
+
[now, id],
|
|
112
|
+
);
|
|
113
|
+
return result.changes > 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function markCreatorContributionFailed(db: Database, id: number, error: string): boolean {
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const result = db.run(
|
|
119
|
+
`UPDATE creator_contribution_staging
|
|
120
|
+
SET status = 'failed', updated_at = ?, last_error = ?
|
|
121
|
+
WHERE id = ? AND status = 'sending'`,
|
|
122
|
+
[now, error, id],
|
|
123
|
+
);
|
|
124
|
+
return result.changes > 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function requeueSendingCreatorContributionSignals(db: Database): number {
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
const result = db.run(
|
|
130
|
+
`UPDATE creator_contribution_staging
|
|
131
|
+
SET status = 'pending', updated_at = ?
|
|
132
|
+
WHERE status = 'sending'`,
|
|
133
|
+
[now],
|
|
134
|
+
);
|
|
135
|
+
return result.changes;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function requeueFailedCreatorContributionSignals(db: Database): number {
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
const result = db.run(
|
|
141
|
+
`UPDATE creator_contribution_staging
|
|
142
|
+
SET status = 'pending', updated_at = ?
|
|
143
|
+
WHERE status = 'failed'`,
|
|
144
|
+
[now],
|
|
145
|
+
);
|
|
146
|
+
return result.changes;
|
|
147
|
+
}
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { CONTRIBUTION_PREFERENCES_PATH, SELFTUNE_CONFIG_DIR } from "./constants.js";
|
|
6
|
+
import {
|
|
7
|
+
discoverCreatorContributionConfigs,
|
|
8
|
+
findCreatorContributionConfig,
|
|
9
|
+
} from "./contribution-config.js";
|
|
10
|
+
import {
|
|
11
|
+
flushCreatorContributionSignals,
|
|
12
|
+
resolveContributionRelayEndpoint,
|
|
13
|
+
} from "./contribution-relay.js";
|
|
14
|
+
import {
|
|
15
|
+
buildContributionPreview,
|
|
16
|
+
type ContributionSignal,
|
|
17
|
+
type ContributionSignalBuildOptions,
|
|
18
|
+
type CreatorContributionRelayPayload,
|
|
19
|
+
} from "./contribution-signals.js";
|
|
20
|
+
import { getDb } from "./localdb/db.js";
|
|
21
|
+
import {
|
|
22
|
+
getCreatorContributionRelayStats,
|
|
23
|
+
getCreatorContributionStagingCounts,
|
|
24
|
+
getSkillTrustSummaries,
|
|
25
|
+
} from "./localdb/queries.js";
|
|
26
|
+
import { CLIError } from "./utils/cli-error.js";
|
|
27
|
+
|
|
28
|
+
export type ContributionGlobalDefault = "ask" | "always" | "never";
|
|
29
|
+
export type ContributionSkillStatus = "opted_in" | "opted_out";
|
|
30
|
+
|
|
31
|
+
export interface ContributionSkillPreference {
|
|
32
|
+
status: ContributionSkillStatus;
|
|
33
|
+
opted_in_at?: string;
|
|
34
|
+
opted_out_at?: string;
|
|
35
|
+
creator_id?: string;
|
|
36
|
+
signals?: ContributionSignal[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ContributionPreferences {
|
|
40
|
+
version: 1;
|
|
41
|
+
global_default: ContributionGlobalDefault;
|
|
42
|
+
skills: Record<string, ContributionSkillPreference>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ContributionPromptCandidate {
|
|
46
|
+
skill_name: string;
|
|
47
|
+
creator_id: string;
|
|
48
|
+
successful_triggers: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_PREFERENCES: ContributionPreferences = {
|
|
52
|
+
version: 1,
|
|
53
|
+
global_default: "ask",
|
|
54
|
+
skills: {},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let cachedPreferences: ContributionPreferences | undefined;
|
|
58
|
+
|
|
59
|
+
function getSelftuneConfigDir(): string {
|
|
60
|
+
return process.env.SELFTUNE_CONFIG_DIR || SELFTUNE_CONFIG_DIR;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getContributionPreferencesPath(): string {
|
|
64
|
+
return process.env.SELFTUNE_CONFIG_DIR
|
|
65
|
+
? join(process.env.SELFTUNE_CONFIG_DIR, "contribution-preferences.json")
|
|
66
|
+
: CONTRIBUTION_PREFERENCES_PATH;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cloneDefaultPreferences(): ContributionPreferences {
|
|
70
|
+
return {
|
|
71
|
+
version: 1,
|
|
72
|
+
global_default: "ask",
|
|
73
|
+
skills: {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isValidGlobalDefault(value: unknown): value is ContributionGlobalDefault {
|
|
78
|
+
return value === "ask" || value === "always" || value === "never";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizePreferences(raw: unknown): ContributionPreferences {
|
|
82
|
+
if (!raw || typeof raw !== "object") return cloneDefaultPreferences();
|
|
83
|
+
const candidate = raw as Partial<ContributionPreferences>;
|
|
84
|
+
const globalDefault = isValidGlobalDefault(candidate.global_default)
|
|
85
|
+
? candidate.global_default
|
|
86
|
+
: DEFAULT_PREFERENCES.global_default;
|
|
87
|
+
const skills: Record<string, ContributionSkillPreference> = {};
|
|
88
|
+
|
|
89
|
+
if (candidate.skills && typeof candidate.skills === "object") {
|
|
90
|
+
for (const [skill, pref] of Object.entries(candidate.skills)) {
|
|
91
|
+
if (!pref || typeof pref !== "object") continue;
|
|
92
|
+
const status = (pref as Partial<ContributionSkillPreference>).status;
|
|
93
|
+
if (status !== "opted_in" && status !== "opted_out") continue;
|
|
94
|
+
skills[skill] = {
|
|
95
|
+
status,
|
|
96
|
+
opted_in_at: (pref as Partial<ContributionSkillPreference>).opted_in_at,
|
|
97
|
+
opted_out_at: (pref as Partial<ContributionSkillPreference>).opted_out_at,
|
|
98
|
+
creator_id:
|
|
99
|
+
typeof (pref as Partial<ContributionSkillPreference>).creator_id === "string"
|
|
100
|
+
? (pref as Partial<ContributionSkillPreference>).creator_id
|
|
101
|
+
: undefined,
|
|
102
|
+
signals: Array.isArray((pref as Partial<ContributionSkillPreference>).signals)
|
|
103
|
+
? (pref as Partial<ContributionSkillPreference>).signals?.filter(
|
|
104
|
+
(signal): signal is ContributionSignal =>
|
|
105
|
+
signal === "trigger" || signal === "grade" || signal === "miss_category",
|
|
106
|
+
)
|
|
107
|
+
: undefined,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
version: 1,
|
|
114
|
+
global_default: globalDefault,
|
|
115
|
+
skills,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadContributionPreferences(): ContributionPreferences {
|
|
120
|
+
if (cachedPreferences) return cachedPreferences;
|
|
121
|
+
const preferencesPath = getContributionPreferencesPath();
|
|
122
|
+
try {
|
|
123
|
+
if (!existsSync(preferencesPath)) {
|
|
124
|
+
cachedPreferences = cloneDefaultPreferences();
|
|
125
|
+
return cachedPreferences;
|
|
126
|
+
}
|
|
127
|
+
const parsed = JSON.parse(readFileSync(preferencesPath, "utf-8")) as unknown;
|
|
128
|
+
cachedPreferences = normalizePreferences(parsed);
|
|
129
|
+
return cachedPreferences;
|
|
130
|
+
} catch {
|
|
131
|
+
cachedPreferences = cloneDefaultPreferences();
|
|
132
|
+
return cachedPreferences;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function saveContributionPreferences(preferences: ContributionPreferences): void {
|
|
137
|
+
mkdirSync(getSelftuneConfigDir(), { recursive: true });
|
|
138
|
+
writeFileSync(getContributionPreferencesPath(), JSON.stringify(preferences, null, 2), "utf-8");
|
|
139
|
+
cachedPreferences = preferences;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resetContributionPreferencesState(): void {
|
|
143
|
+
cachedPreferences = undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function printStatus(preferences: ContributionPreferences): void {
|
|
147
|
+
const discovered = discoverCreatorContributionConfigs();
|
|
148
|
+
const promptCandidates = listContributionPromptCandidates(preferences);
|
|
149
|
+
const relayStats = getCreatorContributionRelayStats(getDb());
|
|
150
|
+
const stagedCounts = new Map(
|
|
151
|
+
getCreatorContributionStagingCounts(getDb()).map((row) => [row.skill_name, row.pending_count]),
|
|
152
|
+
);
|
|
153
|
+
console.log("Creator-directed contributions: configured locally");
|
|
154
|
+
console.log(` Global default: ${preferences.global_default}`);
|
|
155
|
+
console.log(
|
|
156
|
+
` Relay queue: pending=${relayStats.pending} sent=${relayStats.sent} failed=${relayStats.failed}`,
|
|
157
|
+
);
|
|
158
|
+
console.log(` Relay endpoint: ${resolveContributionRelayEndpoint()}`);
|
|
159
|
+
if (discovered.length === 0) {
|
|
160
|
+
console.log(" Installed skill requests: none discovered");
|
|
161
|
+
} else {
|
|
162
|
+
console.log(" Installed skill requests:");
|
|
163
|
+
for (const config of discovered) {
|
|
164
|
+
const pref = preferences.skills[config.skill_name];
|
|
165
|
+
const decision = pref?.status ?? `default (${preferences.global_default})`;
|
|
166
|
+
console.log(` ${config.skill_name}: ${decision}`);
|
|
167
|
+
console.log(` creator: ${config.creator_id}`);
|
|
168
|
+
console.log(` signals: ${config.contribution.signals.join(", ")}`);
|
|
169
|
+
if (config.contribution.message) {
|
|
170
|
+
console.log(` note: ${config.contribution.message}`);
|
|
171
|
+
}
|
|
172
|
+
const staged = stagedCounts.get(config.skill_name) ?? 0;
|
|
173
|
+
if (staged > 0) {
|
|
174
|
+
console.log(` staged locally: ${staged} pending relay signals`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (preferences.global_default !== "ask") {
|
|
180
|
+
console.log(` First-time prompts: skipped (${preferences.global_default} global default)`);
|
|
181
|
+
} else if (promptCandidates.length === 0) {
|
|
182
|
+
console.log(" First-time prompts: none ready");
|
|
183
|
+
} else {
|
|
184
|
+
console.log(" Ready for first-time prompt:");
|
|
185
|
+
for (const candidate of promptCandidates) {
|
|
186
|
+
console.log(
|
|
187
|
+
` ${candidate.skill_name}: ${candidate.successful_triggers} successful triggers (${candidate.creator_id})`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const skillEntries = Object.entries(preferences.skills).sort(([a], [b]) => a.localeCompare(b));
|
|
193
|
+
if (skillEntries.length === 0) {
|
|
194
|
+
console.log(" Explicit overrides: none");
|
|
195
|
+
} else {
|
|
196
|
+
console.log(" Explicit overrides:");
|
|
197
|
+
for (const [skill, pref] of skillEntries) {
|
|
198
|
+
const stamp = pref.status === "opted_in" ? pref.opted_in_at : pref.opted_out_at;
|
|
199
|
+
const when = stamp ? ` (${stamp})` : "";
|
|
200
|
+
console.log(` ${skill}: ${pref.status.replace("_", " ")}${when}`);
|
|
201
|
+
if (pref.creator_id) {
|
|
202
|
+
console.log(` creator: ${pref.creator_id}`);
|
|
203
|
+
}
|
|
204
|
+
if (pref.signals && pref.signals.length > 0) {
|
|
205
|
+
console.log(` signals: ${pref.signals.join(", ")}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
console.log("");
|
|
210
|
+
console.log(
|
|
211
|
+
"These settings apply to creator-directed sharing requests discovered from installed skills.",
|
|
212
|
+
);
|
|
213
|
+
console.log("It does not affect:");
|
|
214
|
+
console.log(" - selftune contribute (community export)");
|
|
215
|
+
console.log(" - selftune push / alpha (your own cloud uploads)");
|
|
216
|
+
console.log(" - selftune contributions upload (creator-directed relay upload)");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function listContributionPromptCandidates(
|
|
220
|
+
preferences: ContributionPreferences = loadContributionPreferences(),
|
|
221
|
+
): ContributionPromptCandidate[] {
|
|
222
|
+
if (preferences.global_default !== "ask") return [];
|
|
223
|
+
|
|
224
|
+
const bySkill = new Map(getSkillTrustSummaries(getDb()).map((row) => [row.skill_name, row]));
|
|
225
|
+
return discoverCreatorContributionConfigs()
|
|
226
|
+
.filter((config) => !preferences.skills[config.skill_name])
|
|
227
|
+
.map((config) => {
|
|
228
|
+
const summary = bySkill.get(config.skill_name);
|
|
229
|
+
return {
|
|
230
|
+
skill_name: config.skill_name,
|
|
231
|
+
creator_id: config.creator_id,
|
|
232
|
+
successful_triggers: summary?.triggered_count ?? 0,
|
|
233
|
+
};
|
|
234
|
+
})
|
|
235
|
+
.filter((candidate) => candidate.successful_triggers > 0)
|
|
236
|
+
.sort(
|
|
237
|
+
(a, b) =>
|
|
238
|
+
b.successful_triggers - a.successful_triggers || a.skill_name.localeCompare(b.skill_name),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function upsertSkillPreference(skill: string, status: ContributionSkillStatus): void {
|
|
243
|
+
const normalizedSkill = skill.trim();
|
|
244
|
+
if (!normalizedSkill) {
|
|
245
|
+
throw new CLIError("Skill name is required.", "INVALID_FLAG", "selftune contributions --help");
|
|
246
|
+
}
|
|
247
|
+
const preferences = loadContributionPreferences();
|
|
248
|
+
const timestamp = new Date().toISOString();
|
|
249
|
+
const discovered = findCreatorContributionConfig(normalizedSkill);
|
|
250
|
+
preferences.skills[normalizedSkill] =
|
|
251
|
+
status === "opted_in"
|
|
252
|
+
? { status, opted_in_at: timestamp }
|
|
253
|
+
: { status, opted_out_at: timestamp };
|
|
254
|
+
if (status === "opted_in" && discovered) {
|
|
255
|
+
preferences.skills[normalizedSkill] = {
|
|
256
|
+
status,
|
|
257
|
+
opted_in_at: timestamp,
|
|
258
|
+
creator_id: discovered.creator_id,
|
|
259
|
+
signals: discovered.contribution.signals.filter(
|
|
260
|
+
(signal): signal is ContributionSignal =>
|
|
261
|
+
signal === "trigger" || signal === "grade" || signal === "miss_category",
|
|
262
|
+
),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
saveContributionPreferences(preferences);
|
|
266
|
+
console.log(
|
|
267
|
+
`Creator-directed contributions for "${normalizedSkill}" ${status === "opted_in" ? "approved" : "revoked"}.`,
|
|
268
|
+
);
|
|
269
|
+
console.log("This only affects future creator-directed sharing prompts and relay uploads.");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildPreviewPayload(
|
|
273
|
+
skill: string,
|
|
274
|
+
options: ContributionSignalBuildOptions = {},
|
|
275
|
+
): {
|
|
276
|
+
config: ReturnType<typeof findCreatorContributionConfig>;
|
|
277
|
+
observedCount: number;
|
|
278
|
+
triggerRate: number | null;
|
|
279
|
+
missRate: number | null;
|
|
280
|
+
gradedSessions: number;
|
|
281
|
+
payload: CreatorContributionRelayPayload;
|
|
282
|
+
} {
|
|
283
|
+
const config = findCreatorContributionConfig(skill);
|
|
284
|
+
if (!config) {
|
|
285
|
+
throw new CLIError(
|
|
286
|
+
`No creator contribution request found for "${skill}".`,
|
|
287
|
+
"FILE_NOT_FOUND",
|
|
288
|
+
"Run `selftune contributions` to see installed skill requests.",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const db = getDb();
|
|
293
|
+
const preview = buildContributionPreview(db, config, options);
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
config,
|
|
297
|
+
observedCount: preview.observedCount,
|
|
298
|
+
triggerRate: preview.triggerRate,
|
|
299
|
+
missRate: preview.missRate,
|
|
300
|
+
gradedSessions: preview.gradedSessions,
|
|
301
|
+
payload: preview.samplePayload,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function printPreview(skill: string): void {
|
|
306
|
+
if (!skill.trim()) {
|
|
307
|
+
throw new CLIError(
|
|
308
|
+
"Skill name is required.",
|
|
309
|
+
"INVALID_FLAG",
|
|
310
|
+
"selftune contributions preview <skill>",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const preview = buildPreviewPayload(skill.trim());
|
|
315
|
+
console.log(`Contribution preview for "${preview.config.skill_name}"`);
|
|
316
|
+
console.log(` creator: ${preview.config.creator_id}`);
|
|
317
|
+
console.log(` requested signals: ${preview.config.contribution.signals.join(", ")}`);
|
|
318
|
+
console.log(" never shared: raw prompts, code/files, your identity");
|
|
319
|
+
console.log(" local coverage:");
|
|
320
|
+
console.log(` trusted checks: ${preview.observedCount}`);
|
|
321
|
+
if (preview.triggerRate != null) {
|
|
322
|
+
console.log(` trigger rate: ${preview.triggerRate}%`);
|
|
323
|
+
}
|
|
324
|
+
if (preview.missRate != null) {
|
|
325
|
+
console.log(` miss rate: ${preview.missRate}%`);
|
|
326
|
+
}
|
|
327
|
+
console.log(` graded sessions: ${preview.gradedSessions}`);
|
|
328
|
+
console.log("");
|
|
329
|
+
console.log("Example relay payload:");
|
|
330
|
+
console.log(JSON.stringify(preview.payload, null, 2));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function setGlobalDefault(value: string | undefined): void {
|
|
334
|
+
if (!isValidGlobalDefault(value)) {
|
|
335
|
+
throw new CLIError(
|
|
336
|
+
`Invalid default: ${value ?? "(none)"}`,
|
|
337
|
+
"INVALID_FLAG",
|
|
338
|
+
"selftune contributions default <ask|always|never>",
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const preferences = loadContributionPreferences();
|
|
342
|
+
preferences.global_default = value;
|
|
343
|
+
saveContributionPreferences(preferences);
|
|
344
|
+
console.log(`Creator-directed contributions default set to: ${value}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function resetPreferences(): void {
|
|
348
|
+
saveContributionPreferences(cloneDefaultPreferences());
|
|
349
|
+
console.log("Creator-directed contribution preferences reset to defaults.");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
interface ContributionsUploadArgs {
|
|
353
|
+
dryRun: boolean;
|
|
354
|
+
retryFailed: boolean;
|
|
355
|
+
limit?: number;
|
|
356
|
+
endpoint?: string;
|
|
357
|
+
apiKey?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseUploadArgs(argv: string[]): ContributionsUploadArgs {
|
|
361
|
+
const parsed: ContributionsUploadArgs = { dryRun: false, retryFailed: false };
|
|
362
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
363
|
+
const token = argv[index];
|
|
364
|
+
switch (token) {
|
|
365
|
+
case "--dry-run":
|
|
366
|
+
parsed.dryRun = true;
|
|
367
|
+
break;
|
|
368
|
+
case "--retry-failed":
|
|
369
|
+
parsed.retryFailed = true;
|
|
370
|
+
break;
|
|
371
|
+
case "--limit": {
|
|
372
|
+
const value = argv[index + 1];
|
|
373
|
+
if (!value) {
|
|
374
|
+
throw new CLIError(
|
|
375
|
+
"Missing value for --limit.",
|
|
376
|
+
"INVALID_FLAG",
|
|
377
|
+
"selftune contributions upload --help",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const limit = Number.parseInt(value, 10);
|
|
381
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
382
|
+
throw new CLIError(
|
|
383
|
+
`Invalid limit: ${value}`,
|
|
384
|
+
"INVALID_FLAG",
|
|
385
|
+
"selftune contributions upload --help",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
parsed.limit = limit;
|
|
389
|
+
index += 1;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "--endpoint":
|
|
393
|
+
parsed.endpoint = argv[index + 1];
|
|
394
|
+
if (!parsed.endpoint) {
|
|
395
|
+
throw new CLIError(
|
|
396
|
+
"Missing value for --endpoint.",
|
|
397
|
+
"INVALID_FLAG",
|
|
398
|
+
"selftune contributions upload --help",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
index += 1;
|
|
402
|
+
break;
|
|
403
|
+
case "--api-key":
|
|
404
|
+
parsed.apiKey = argv[index + 1];
|
|
405
|
+
if (!parsed.apiKey) {
|
|
406
|
+
throw new CLIError(
|
|
407
|
+
"Missing value for --api-key.",
|
|
408
|
+
"INVALID_FLAG",
|
|
409
|
+
"selftune contributions upload --help",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
index += 1;
|
|
413
|
+
break;
|
|
414
|
+
case "--help":
|
|
415
|
+
case "-h":
|
|
416
|
+
console.log(`selftune contributions upload — Flush staged creator-directed relay signals
|
|
417
|
+
|
|
418
|
+
Usage:
|
|
419
|
+
selftune contributions upload [--dry-run] [--retry-failed] [--limit <n>] [--endpoint <url>] [--api-key <key>]
|
|
420
|
+
|
|
421
|
+
Options:
|
|
422
|
+
--dry-run Preview how many staged signals would upload
|
|
423
|
+
--retry-failed Requeue previously failed rows before attempting upload
|
|
424
|
+
--limit <n> Max number of staged rows to attempt (default: 50)
|
|
425
|
+
--endpoint <url> Override relay endpoint (default: ${resolveContributionRelayEndpoint()})
|
|
426
|
+
--api-key <key> Override cloud API key (defaults to config.alpha.api_key)`);
|
|
427
|
+
process.exit(0);
|
|
428
|
+
default:
|
|
429
|
+
throw new CLIError(
|
|
430
|
+
`Unknown contributions upload flag: ${token}`,
|
|
431
|
+
"INVALID_FLAG",
|
|
432
|
+
"selftune contributions upload --help",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return parsed;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function uploadContributions(argv: string[]): Promise<void> {
|
|
440
|
+
const args = parseUploadArgs(argv);
|
|
441
|
+
const result = await flushCreatorContributionSignals(getDb(), args);
|
|
442
|
+
if (args.dryRun) {
|
|
443
|
+
console.log("Creator-directed relay upload dry run");
|
|
444
|
+
console.log(` endpoint: ${result.endpoint}`);
|
|
445
|
+
console.log(` pending rows considered: ${result.attempted}`);
|
|
446
|
+
console.log(` requeued stale sending rows: ${result.requeued}`);
|
|
447
|
+
if (result.retried_failed > 0) {
|
|
448
|
+
console.log(` failed rows requeued: ${result.retried_failed}`);
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
console.log("Creator-directed relay upload complete");
|
|
454
|
+
console.log(` endpoint: ${result.endpoint}`);
|
|
455
|
+
console.log(` attempted: ${result.attempted}`);
|
|
456
|
+
console.log(` sent: ${result.sent}`);
|
|
457
|
+
console.log(` failed: ${result.failed}`);
|
|
458
|
+
if (result.requeued > 0) {
|
|
459
|
+
console.log(` requeued stale sending rows: ${result.requeued}`);
|
|
460
|
+
}
|
|
461
|
+
if (result.retried_failed > 0) {
|
|
462
|
+
console.log(` failed rows requeued: ${result.retried_failed}`);
|
|
463
|
+
}
|
|
464
|
+
console.log(
|
|
465
|
+
` queue now: pending=${result.stats.pending} sent=${result.stats.sent} failed=${result.stats.failed}`,
|
|
466
|
+
);
|
|
467
|
+
if (result.failed > 0) {
|
|
468
|
+
process.exitCode = 1;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export async function cliMain(): Promise<void> {
|
|
473
|
+
const sub = process.argv[2];
|
|
474
|
+
const arg = process.argv[3];
|
|
475
|
+
|
|
476
|
+
if (sub === "--help" || sub === "-h") {
|
|
477
|
+
console.log(`selftune contributions — Manage creator-directed sharing preferences
|
|
478
|
+
|
|
479
|
+
Usage:
|
|
480
|
+
selftune contributions
|
|
481
|
+
selftune contributions status
|
|
482
|
+
selftune contributions preview <skill>
|
|
483
|
+
selftune contributions approve <skill>
|
|
484
|
+
selftune contributions revoke <skill>
|
|
485
|
+
selftune contributions default <ask|always|never>
|
|
486
|
+
selftune contributions upload [--dry-run] [--retry-failed] [--limit <n>] [--endpoint <url>] [--api-key <key>]
|
|
487
|
+
selftune contributions reset
|
|
488
|
+
|
|
489
|
+
Purpose:
|
|
490
|
+
Tracks local opt-in / opt-out state for creator-directed contribution
|
|
491
|
+
flows discovered from installed skills. This is separate from:
|
|
492
|
+
selftune contribute Community export bundle
|
|
493
|
+
selftune alpha upload Personal cloud upload cycle
|
|
494
|
+
|
|
495
|
+
Uploads:
|
|
496
|
+
Approved skills stage privacy-safe relay rows locally during sync.
|
|
497
|
+
Use 'selftune contributions upload' to flush those staged rows to the
|
|
498
|
+
creator-directed relay endpoint.`);
|
|
499
|
+
process.exit(0);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
switch (sub) {
|
|
503
|
+
case undefined:
|
|
504
|
+
case "status":
|
|
505
|
+
printStatus(loadContributionPreferences());
|
|
506
|
+
break;
|
|
507
|
+
case "preview":
|
|
508
|
+
printPreview(arg ?? "");
|
|
509
|
+
break;
|
|
510
|
+
case "approve":
|
|
511
|
+
upsertSkillPreference(arg ?? "", "opted_in");
|
|
512
|
+
break;
|
|
513
|
+
case "revoke":
|
|
514
|
+
upsertSkillPreference(arg ?? "", "opted_out");
|
|
515
|
+
break;
|
|
516
|
+
case "default":
|
|
517
|
+
setGlobalDefault(arg);
|
|
518
|
+
break;
|
|
519
|
+
case "upload":
|
|
520
|
+
await uploadContributions(process.argv.slice(3));
|
|
521
|
+
break;
|
|
522
|
+
case "reset":
|
|
523
|
+
resetPreferences();
|
|
524
|
+
break;
|
|
525
|
+
default:
|
|
526
|
+
throw new CLIError(
|
|
527
|
+
`Unknown contributions subcommand: ${sub}`,
|
|
528
|
+
"INVALID_FLAG",
|
|
529
|
+
"selftune contributions --help",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|