@fraylabs/possible 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraylabs/possible",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Create, publish, discover, and reuse source-owned AI Outcomes",
5
5
  "keywords": [
6
6
  "codex",
package/src/directory.mjs CHANGED
@@ -1,4 +1,9 @@
1
- const DEFAULT_ENDPOINT = "https://abutwsaahahtbtlopczi.supabase.co/functions/v1/outcome-directory";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+
6
+ const DEFAULT_ENDPOINT = "https://reminiscent-lark-333.convex.site/api/outcomes";
2
7
 
3
8
  const directoryEndpoint = () => process.env.POSSIBLE_DIRECTORY_ENDPOINT?.trim() || DEFAULT_ENDPOINT;
4
9
 
@@ -11,6 +16,53 @@ async function requestDirectory(parameters, fetchImplementation = fetch) {
11
16
  return body;
12
17
  }
13
18
 
19
+ function installationPath(environment = process.env) {
20
+ const home = environment.POSSIBLE_HOME ? resolve(environment.POSSIBLE_HOME) : join(homedir(), ".possible");
21
+ return join(home, "installation.json");
22
+ }
23
+
24
+ async function installationToken(environment = process.env) {
25
+ const path = installationPath(environment);
26
+ try {
27
+ const stored = JSON.parse(await readFile(path, "utf8"));
28
+ if (stored?.schemaVersion === 1 && typeof stored.id === "string") return stored.id;
29
+ } catch (error) {
30
+ if (error?.code !== "ENOENT" && !(error instanceof SyntaxError)) return undefined;
31
+ }
32
+ const id = randomUUID();
33
+ try {
34
+ await mkdir(dirname(path), { recursive: true });
35
+ await writeFile(path, `${JSON.stringify({ schemaVersion: 1, id }, null, 2)}\n`, { flag: "wx" });
36
+ return id;
37
+ } catch (error) {
38
+ if (error?.code === "EEXIST") return installationToken(environment);
39
+ return undefined;
40
+ }
41
+ }
42
+
43
+ async function recordUse(outcomeId, { fetchImplementation = fetch, environment = process.env } = {}) {
44
+ if (environment.CI || environment.POSSIBLE_TELEMETRY === "0") return;
45
+ const token = await installationToken(environment);
46
+ if (!token) return;
47
+ const endpoint = new URL(directoryEndpoint());
48
+ endpoint.pathname = `${endpoint.pathname.replace(/\/$/, "")}/use`;
49
+ endpoint.search = "";
50
+ try {
51
+ await fetchImplementation(endpoint, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json", accept: "application/json" },
54
+ body: JSON.stringify({
55
+ outcomeId,
56
+ visitorHash: createHash("sha256").update(token).digest("hex"),
57
+ source: "cli",
58
+ ci: false,
59
+ }),
60
+ });
61
+ } catch {
62
+ // Usage reporting never blocks access to a published prompt.
63
+ }
64
+ }
65
+
14
66
  export async function searchOutcomes(query, options = {}) {
15
67
  const normalized = String(query ?? "").trim();
16
68
  if (!normalized) throw new Error("Search requires an ordinary-language query");
@@ -23,6 +75,7 @@ export async function fetchOutcome(id, options = {}) {
23
75
  if (!normalized) throw new Error("Fetch requires an Outcome ID from search results");
24
76
  const body = await requestDirectory({ id: normalized }, options.fetchImplementation);
25
77
  if (!body.outcome || typeof body.outcome.prompt !== "string") throw new Error("Possible returned an invalid Outcome");
78
+ await recordUse(body.outcome.id, options);
26
79
  return body.outcome;
27
80
  }
28
81
 
@@ -31,6 +84,7 @@ export function formatSearchResults(outcomes) {
31
84
  return `${outcomes.map((outcome, index) => [
32
85
  `${index + 1}. ${outcome.title}`,
33
86
  ` ${outcome.summary}`,
87
+ ...(outcome.primary_attribution ? [` Primary: ${outcome.primary_attribution.kind === "product" ? outcome.primary_attribution.id : `${outcome.primary_attribution.repository}/${outcome.primary_attribution.directory}`}`] : []),
34
88
  ` ID: ${outcome.id}`,
35
89
  ` Source: ${outcome.source_locator}`,
36
90
  ].join("\n")).join("\n\n")}\n`;
package/src/index.mjs CHANGED
@@ -8,7 +8,8 @@ import { addOutcomeSource, createOutcome, publishOutcomeSource, useOutcome, vali
8
8
  const HELP = `Possible CLI
9
9
 
10
10
  Usage:
11
- possible create <slug>
11
+ possible create <slug> --product <owner/product>
12
+ possible create <slug> --skill <owner/repository> <directory> <commit>
12
13
  possible validate [directory]
13
14
  possible publish [owner/repository | https://publisher.example]
14
15
  possible search <ordinary-language query>
@@ -18,7 +19,7 @@ Usage:
18
19
  possible bookmark <command>
19
20
 
20
21
  Commands:
21
- create Create outcome.json, outcome.md, prompt.md, and media/ for one Outcome
22
+ create Create one Outcome with its required primary Product or Skill
22
23
  validate Validate every Outcome folder below a directory
23
24
  publish Validate and submit one public publisher source; no account required
24
25
  search Find relevant Outcomes in the live public directory
@@ -56,9 +57,12 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
56
57
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
57
58
  process.exitCode = 1;
58
59
  }
59
- } else if (args[0] === "create" && args.length === 2) {
60
+ } else if (args[0] === "create" && ((args[2] === "--product" && args.length === 4) || (args[2] === "--skill" && args.length === 6))) {
60
61
  try {
61
- const folder = await createOutcome(args[1]);
62
+ const primary = args[2] === "--product"
63
+ ? { kind: "product", id: args[3] }
64
+ : { kind: "skill", repository: args[3], directory: args[4], lastReviewedCommit: args[5] };
65
+ const folder = await createOutcome(args[1], { primary });
62
66
  process.stdout.write(`Created ${folder}\n`);
63
67
  } catch (error) {
64
68
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -7,10 +7,11 @@ import { discoverOutcomeSource } from "./sources.mjs";
7
7
 
8
8
  const execFileAsync = promisify(execFile);
9
9
  const SAFE_SLUG = /^[a-z0-9][a-z0-9-]*$/;
10
- const DEFAULT_PUBLISH_ENDPOINT = "https://abutwsaahahtbtlopczi.supabase.co/functions/v1/register-outcome-source";
10
+ const DEFAULT_PUBLISH_ENDPOINT = "https://reminiscent-lark-333.convex.site/api/outcomes/register";
11
11
 
12
- export async function createOutcome(slug, { directory = process.cwd() } = {}) {
12
+ export async function createOutcome(slug, { directory = process.cwd(), primary } = {}) {
13
13
  if (!SAFE_SLUG.test(slug ?? "")) throw new Error("Outcome slug must be lowercase and hyphenated");
14
+ if (!primary) throw new Error("Choose one primary attribution with --product or --skill");
14
15
  const root = resolve(directory);
15
16
  const indexPath = join(root, "outcomes.json");
16
17
  let publisherIndex = {
@@ -28,13 +29,14 @@ export async function createOutcome(slug, { directory = process.cwd() } = {}) {
28
29
  await mkdir(join(folder, "media"), { recursive: true });
29
30
  const manifestPath = join(folder, "outcome.json");
30
31
  const manifest = {
31
- schemaVersion: 3,
32
+ schemaVersion: 4,
32
33
  slug,
33
34
  files: { about: "outcome.md", prompt: "prompt.md" },
34
35
  authoredAt: null,
35
36
  author: { name: "Replace with publisher name", url: "https://example.com" },
36
37
  models: [{ provider: "Replace with provider", model: "Replace with model", role: "execution" }],
37
38
  requirements: [],
39
+ primary,
38
40
  };
39
41
  await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { flag: "wx" });
40
42
  await writeFile(join(folder, "outcome.md"), `# Replace with Outcome name\n\nDescribe the concrete result in one clear opening paragraph.\n`, { flag: "wx" });
@@ -8,7 +8,9 @@ const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
8
8
  const SAFE_REPOSITORY_PATH = /^(?:\.|[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*)$/;
9
9
  const MODEL_ROLES = new Set(["authorship", "execution", "review"]);
10
10
  const FILE_TYPES = new Set(["image", "video", "audio", "cad", "document", "data", "source", "archive", "other"]);
11
- const MANIFEST_KEYS = new Set(["schemaVersion", "slug", "files", "authoredAt", "author", "models", "requirements", "products", "skills", "inputs", "artifacts", "preview"]);
11
+ const BASE_MANIFEST_KEYS = ["schemaVersion", "slug", "files", "authoredAt", "author", "models", "requirements", "inputs", "artifacts", "preview"];
12
+ const LEGACY_MANIFEST_KEYS = new Set([...BASE_MANIFEST_KEYS, "products", "skills"]);
13
+ const MANIFEST_KEYS = new Set([...BASE_MANIFEST_KEYS, "primary", "secondary"]);
12
14
 
13
15
  const asObject = (value, context) => {
14
16
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${context} must be a JSON object`);
@@ -56,10 +58,59 @@ const validateFiles = (value, context) => {
56
58
  });
57
59
  };
58
60
 
61
+ const validateSkillReference = (value, context, includeKind = false) => {
62
+ const skill = asObject(value, context);
63
+ exactKeys(skill, new Set(includeKind ? ["kind", "repository", "lastReviewedCommit", "directory"] : ["repository", "lastReviewedCommit", "directory"]), context);
64
+ if (includeKind && skill.kind !== "skill") throw new Error(`${context}.kind must be skill`);
65
+ if (!GITHUB_REPOSITORY.test(string(skill.repository, `${context}.repository`))) throw new Error(`${context}.repository is invalid`);
66
+ if (!EXACT_REVISION.test(string(skill.lastReviewedCommit, `${context}.lastReviewedCommit`))) throw new Error(`${context}.lastReviewedCommit must be an exact commit`);
67
+ const directory = string(skill.directory, `${context}.directory`);
68
+ if (!SAFE_REPOSITORY_PATH.test(directory) || (directory !== "." && directory.split("/").some((part) => part === "." || part === ".."))) throw new Error(`${context}.directory is invalid`);
69
+ return skill;
70
+ };
71
+
72
+ const validateAttribution = (value, context) => {
73
+ const attribution = asObject(value, context);
74
+ if (attribution.kind === "product") {
75
+ exactKeys(attribution, new Set(["kind", "id"]), context);
76
+ if (!PRODUCT_ID.test(string(attribution.id, `${context}.id`))) throw new Error(`${context}.id is invalid`);
77
+ return attribution;
78
+ }
79
+ if (attribution.kind === "skill") return validateSkillReference(attribution, context, true);
80
+ throw new Error(`${context}.kind must be product or skill`);
81
+ };
82
+
83
+ export const attributionKey = (attribution) => attribution.kind === "product"
84
+ ? `product:${attribution.id}`
85
+ : `skill:${attribution.repository}/${attribution.directory}`;
86
+
87
+ export function normalizeOutcomeAttributions(manifest) {
88
+ if (manifest.schemaVersion === 4) {
89
+ const secondary = manifest.secondary ?? [];
90
+ const all = [manifest.primary, ...secondary];
91
+ return {
92
+ primary: manifest.primary,
93
+ secondary,
94
+ products: all.filter((item) => item.kind === "product").map((item) => item.id),
95
+ skills: all.filter((item) => item.kind === "skill").map(({ kind: _kind, ...skill }) => skill),
96
+ };
97
+ }
98
+ const legacy = [
99
+ ...(manifest.products ?? []).map((id) => ({ kind: "product", id })),
100
+ ...(manifest.skills ?? []).map((skill) => ({ kind: "skill", ...skill })),
101
+ ];
102
+ return {
103
+ primary: legacy[0] ?? null,
104
+ secondary: legacy.slice(1),
105
+ products: manifest.products ?? [],
106
+ skills: manifest.skills ?? [],
107
+ };
108
+ }
109
+
59
110
  export function validateOutcomeManifest(value, context = "outcome.json") {
60
111
  const manifest = asObject(value, context);
61
- exactKeys(manifest, MANIFEST_KEYS, context);
62
- if (manifest.schemaVersion !== 3) throw new Error(`${context}.schemaVersion must be 3`);
112
+ if (manifest.schemaVersion !== 3 && manifest.schemaVersion !== 4) throw new Error(`${context}.schemaVersion must be 3 or 4`);
113
+ exactKeys(manifest, manifest.schemaVersion === 4 ? MANIFEST_KEYS : LEGACY_MANIFEST_KEYS, context);
63
114
  if (!SAFE_SLUG.test(string(manifest.slug, `${context}.slug`))) throw new Error(`${context}.slug must be lowercase and hyphenated`);
64
115
 
65
116
  const files = asObject(manifest.files, `${context}.files`);
@@ -92,24 +143,27 @@ export function validateOutcomeManifest(value, context = "outcome.json") {
92
143
  const requirements = manifest.requirements.map((entry, index) => string(entry, `${context}.requirements[${index}]`));
93
144
  if (new Set(requirements).size !== requirements.length) throw new Error(`${context}.requirements contains duplicates`);
94
145
 
95
- if (manifest.products !== undefined) {
146
+ if (manifest.schemaVersion === 3 && manifest.products !== undefined) {
96
147
  if (!Array.isArray(manifest.products) || manifest.products.length === 0) throw new Error(`${context}.products must be omitted or a non-empty array`);
97
148
  for (const [index, product] of manifest.products.entries()) if (!PRODUCT_ID.test(string(product, `${context}.products[${index}]`))) throw new Error(`${context}.products[${index}] is invalid`);
98
149
  if (new Set(manifest.products).size !== manifest.products.length) throw new Error(`${context}.products contains duplicates`);
99
150
  }
100
151
 
101
- if (manifest.skills !== undefined) {
152
+ if (manifest.schemaVersion === 3 && manifest.skills !== undefined) {
102
153
  if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) throw new Error(`${context}.skills must be omitted or a non-empty array`);
103
154
  for (const [index, entry] of manifest.skills.entries()) {
104
- const skill = asObject(entry, `${context}.skills[${index}]`);
105
- exactKeys(skill, new Set(["repository", "lastReviewedCommit", "directory"]), `${context}.skills[${index}]`);
106
- if (!GITHUB_REPOSITORY.test(string(skill.repository, `${context}.skills[${index}].repository`))) throw new Error(`${context}.skills[${index}].repository is invalid`);
107
- if (!EXACT_REVISION.test(string(skill.lastReviewedCommit, `${context}.skills[${index}].lastReviewedCommit`))) throw new Error(`${context}.skills[${index}].lastReviewedCommit must be an exact commit`);
108
- const directory = string(skill.directory, `${context}.skills[${index}].directory`);
109
- if (!SAFE_REPOSITORY_PATH.test(directory) || (directory !== "." && directory.split("/").some((part) => part === "." || part === ".."))) throw new Error(`${context}.skills[${index}].directory is invalid`);
155
+ validateSkillReference(entry, `${context}.skills[${index}]`);
110
156
  }
111
157
  }
112
158
 
159
+ if (manifest.schemaVersion === 4) {
160
+ const primary = validateAttribution(manifest.primary, `${context}.primary`);
161
+ if (manifest.secondary !== undefined && (!Array.isArray(manifest.secondary) || manifest.secondary.length === 0)) throw new Error(`${context}.secondary must be omitted or a non-empty array`);
162
+ const secondary = (manifest.secondary ?? []).map((entry, index) => validateAttribution(entry, `${context}.secondary[${index}]`));
163
+ const keys = [attributionKey(primary), ...secondary.map(attributionKey)];
164
+ if (new Set(keys).size !== keys.length) throw new Error(`${context} contains duplicate primary or secondary attributions`);
165
+ }
166
+
113
167
  validateFiles(manifest.inputs, `${context}.inputs`);
114
168
  validateFiles(manifest.artifacts, `${context}.artifacts`);
115
169
  if (manifest.preview !== undefined) asObject(manifest.preview, `${context}.preview`);
@@ -0,0 +1,36 @@
1
+ export type OutcomeSource = {
2
+ type: "github" | "well-known";
3
+ locator: string;
4
+ installUrl: string;
5
+ };
6
+
7
+ export type DiscoveredOutcome = {
8
+ slug: string;
9
+ title: string;
10
+ summary: string;
11
+ aboutMarkdown: string;
12
+ prompt: string;
13
+ manifest: Record<string, unknown>;
14
+ manifestUrl: string;
15
+ aboutUrl: string;
16
+ promptUrl: string;
17
+ repositoryPath?: string;
18
+ contentHash: string;
19
+ };
20
+
21
+ export type OutcomeDiscovery = OutcomeSource & {
22
+ revision: string;
23
+ publisherName: string;
24
+ outcomes: DiscoveredOutcome[];
25
+ };
26
+
27
+ export type PublicOutcomeSnapshot = {
28
+ schemaVersion: 1;
29
+ source: OutcomeSource & { revision: string };
30
+ publisherName: string;
31
+ outcomes: DiscoveredOutcome[];
32
+ };
33
+
34
+ export function parseOutcomeSource(value: string): OutcomeSource;
35
+ export function discoverOutcomeSource(value: string | OutcomeSource, options?: { githubToken?: string }): Promise<OutcomeDiscovery>;
36
+ export function publicSnapshot(discovery: OutcomeDiscovery): PublicOutcomeSnapshot;