@ikenga/contract 0.3.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 +38 -0
- package/dist/artifact.d.ts +1249 -0
- package/dist/artifact.d.ts.map +1 -0
- package/dist/artifact.js +164 -0
- package/dist/artifact.js.map +1 -0
- package/dist/engine.d.ts +65 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +7 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/iyke.d.ts +21 -0
- package/dist/iyke.d.ts.map +1 -0
- package/dist/iyke.js +38 -0
- package/dist/iyke.js.map +1 -0
- package/dist/manifest.d.ts +917 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +158 -0
- package/dist/manifest.js.map +1 -0
- package/dist/rpc.d.ts +130 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +8 -0
- package/dist/rpc.js.map +1 -0
- package/dist/scopes.d.ts +11 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +56 -0
- package/dist/scopes.js.map +1 -0
- package/package.json +67 -0
- package/schemas/artifact/v0.json +638 -0
- package/src/artifact-fixtures/ceo-overview.json +56 -0
- package/src/artifact-fixtures/cfo-daily.json +42 -0
- package/src/artifact-fixtures/hello-world.json +25 -0
- package/src/artifact.test.ts +44 -0
- package/src/artifact.ts +197 -0
- package/src/engine.ts +57 -0
- package/src/index.ts +9 -0
- package/src/iyke.ts +43 -0
- package/src/manifest.ts +185 -0
- package/src/rpc.ts +97 -0
- package/src/scopes.ts +71 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "ikenga-artifact",
|
|
3
|
+
"formatVersion": "0.1",
|
|
4
|
+
"id": "cfo-daily",
|
|
5
|
+
"name": "CFO Daily",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Cash position, AR aging, FX, and overnight cron status. Refresh ladder per source.",
|
|
8
|
+
"license": "BUSL-1.1",
|
|
9
|
+
"icon": { "lucide": "wallet" },
|
|
10
|
+
"dataSources": {
|
|
11
|
+
"cash": {
|
|
12
|
+
"type": "supabase",
|
|
13
|
+
"table": "v_cash_position",
|
|
14
|
+
"select": "*",
|
|
15
|
+
"refresh": { "mode": "interval", "every": "15m", "onFocus": true }
|
|
16
|
+
},
|
|
17
|
+
"ar_aging": {
|
|
18
|
+
"type": "sql",
|
|
19
|
+
"db": "ikenga.local",
|
|
20
|
+
"query": "select * from ar_aging_view",
|
|
21
|
+
"refresh": { "mode": "manual" }
|
|
22
|
+
},
|
|
23
|
+
"fx": {
|
|
24
|
+
"type": "fetch",
|
|
25
|
+
"url": "https://open.er-api.com/v6/latest/USD",
|
|
26
|
+
"refresh": { "mode": "interval", "every": "1h" }
|
|
27
|
+
},
|
|
28
|
+
"cron_status": {
|
|
29
|
+
"type": "file",
|
|
30
|
+
"path": "./data/last_run.json",
|
|
31
|
+
"refresh": { "mode": "watch" }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"fallback": {
|
|
35
|
+
"mode": "mock",
|
|
36
|
+
"dataTag": "ikenga-mock-data",
|
|
37
|
+
"banner": "Showing mock data — live sources unavailable."
|
|
38
|
+
},
|
|
39
|
+
"pin": { "suggested": true, "section": "finance", "label": "CFO Daily" },
|
|
40
|
+
"notes": { "enabled": true },
|
|
41
|
+
"requires": { "ikenga": ">=0.4", "bridge": "^1.0" }
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "ikenga-artifact",
|
|
3
|
+
"formatVersion": "0.1",
|
|
4
|
+
"id": "hello-world",
|
|
5
|
+
"name": "Hello, Artifact",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Smallest valid Ikenga artifact. Lists users from a public API; falls back to mock when offline.",
|
|
8
|
+
"license": "BUSL-1.1",
|
|
9
|
+
"icon": { "lucide": "sparkles" },
|
|
10
|
+
"dataSources": {
|
|
11
|
+
"users": {
|
|
12
|
+
"type": "fetch",
|
|
13
|
+
"url": "https://jsonplaceholder.typicode.com/users",
|
|
14
|
+
"refresh": { "mode": "manual" }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"fallback": {
|
|
18
|
+
"mode": "mock",
|
|
19
|
+
"dataTag": "ikenga-mock-data",
|
|
20
|
+
"banner": "Showing mock data — live source unreachable."
|
|
21
|
+
},
|
|
22
|
+
"pin": { "suggested": false },
|
|
23
|
+
"notes": { "enabled": true },
|
|
24
|
+
"requires": { "ikenga": ">=0.4", "bridge": "^1.0" }
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { ArtifactManifestSchema } from './artifact.js';
|
|
7
|
+
|
|
8
|
+
const fixturesDir = path.resolve(
|
|
9
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
'artifact-fixtures',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
test('all fixtures parse against the manifest schema', async () => {
|
|
14
|
+
const files = (await readdir(fixturesDir)).filter((f) => f.endsWith('.json'));
|
|
15
|
+
assert.ok(files.length >= 3, `expected >=3 fixtures, got ${files.length}`);
|
|
16
|
+
for (const f of files) {
|
|
17
|
+
const json = JSON.parse(await readFile(path.join(fixturesDir, f), 'utf8'));
|
|
18
|
+
const result = ArtifactManifestSchema.safeParse(json);
|
|
19
|
+
assert.ok(
|
|
20
|
+
result.success,
|
|
21
|
+
`${f} failed to parse: ${
|
|
22
|
+
result.success ? '' : JSON.stringify(result.error.issues, null, 2)
|
|
23
|
+
}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('rejects manifest with missing required fields', () => {
|
|
29
|
+
const result = ArtifactManifestSchema.safeParse({ format: 'ikenga-artifact' });
|
|
30
|
+
assert.equal(result.success, false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('rejects non-kebab-case id', () => {
|
|
34
|
+
const result = ArtifactManifestSchema.safeParse({
|
|
35
|
+
format: 'ikenga-artifact',
|
|
36
|
+
formatVersion: '0.1',
|
|
37
|
+
id: 'NotKebab',
|
|
38
|
+
name: 'X',
|
|
39
|
+
version: '0.1.0',
|
|
40
|
+
dataSources: {},
|
|
41
|
+
fallback: { mode: 'mock', dataTag: 'x' },
|
|
42
|
+
});
|
|
43
|
+
assert.equal(result.success, false);
|
|
44
|
+
});
|
package/src/artifact.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Ikenga artifact manifest schema (v0.1).
|
|
2
|
+
//
|
|
3
|
+
// The artifact format is the small, agent-authorable single-file (or folder)
|
|
4
|
+
// HTML doc that renders standalone in any browser AND lights up with live
|
|
5
|
+
// data inside the Ikenga shell. This Zod schema is the source of truth for
|
|
6
|
+
// the manifest block embedded in `<script type="application/json"
|
|
7
|
+
// id="ikenga-manifest">…</script>` (single-file mode) or in
|
|
8
|
+
// `manifest.json` (folder mode).
|
|
9
|
+
//
|
|
10
|
+
// JSON Schema is generated from this file via `pnpm generate:schemas`
|
|
11
|
+
// into `schemas/artifact/v0.json` and published at:
|
|
12
|
+
// https://royalti-io.github.io/ikenga-contract/schemas/artifact/v0.json
|
|
13
|
+
//
|
|
14
|
+
// NOTE: this is **unrelated** to the pkg manifest in `./manifest.ts`. They
|
|
15
|
+
// describe different things (pkgs are Tauri-side mini-apps; artifacts are
|
|
16
|
+
// portable HTML docs). Don't conflate them.
|
|
17
|
+
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
|
|
20
|
+
// ---------- Refresh modes ----------
|
|
21
|
+
|
|
22
|
+
export const RefreshSchema = z.discriminatedUnion('mode', [
|
|
23
|
+
z.object({ mode: z.literal('manual') }),
|
|
24
|
+
z.object({
|
|
25
|
+
mode: z.literal('interval'),
|
|
26
|
+
/** Duration string: `<number><unit>` where unit ∈ s|m|h|d. e.g. "30s", "15m", "1h", "1d". */
|
|
27
|
+
every: z.string().regex(/^\d+\s*(s|m|h|d)$/),
|
|
28
|
+
/** When true, also refresh whenever the artifact's pane regains focus. */
|
|
29
|
+
onFocus: z.boolean().optional(),
|
|
30
|
+
}),
|
|
31
|
+
/** File / realtime sources only — bridge subscribes via fs_watch or equivalent. */
|
|
32
|
+
z.object({ mode: z.literal('watch') }),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// ---------- Data sources ----------
|
|
36
|
+
|
|
37
|
+
const SourceSupabase = z.object({
|
|
38
|
+
type: z.literal('supabase'),
|
|
39
|
+
table: z.string(),
|
|
40
|
+
select: z.string().optional(),
|
|
41
|
+
/** PostgREST-style filters: `[column, operator, value][]`. */
|
|
42
|
+
filter: z.array(z.tuple([z.string(), z.string(), z.unknown()])).optional(),
|
|
43
|
+
refresh: RefreshSchema,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const SourceSql = z.object({
|
|
47
|
+
type: z.literal('sql'),
|
|
48
|
+
/** Logical DB name (e.g. `ikenga.local`). Resolved by the host. */
|
|
49
|
+
db: z.string(),
|
|
50
|
+
query: z.string(),
|
|
51
|
+
params: z.array(z.unknown()).optional(),
|
|
52
|
+
refresh: RefreshSchema,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const SourceFetch = z.object({
|
|
56
|
+
type: z.literal('fetch'),
|
|
57
|
+
url: z.string().url(),
|
|
58
|
+
method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).optional(),
|
|
59
|
+
headers: z.record(z.string()).optional(),
|
|
60
|
+
refresh: RefreshSchema,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const SourceMcp = z.object({
|
|
64
|
+
type: z.literal('mcp'),
|
|
65
|
+
server: z.string(),
|
|
66
|
+
tool: z.string(),
|
|
67
|
+
args: z.record(z.unknown()).optional(),
|
|
68
|
+
refresh: RefreshSchema,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const SourceFile = z.object({
|
|
72
|
+
type: z.literal('file'),
|
|
73
|
+
path: z.string(),
|
|
74
|
+
refresh: RefreshSchema,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const DataSourceSchema = z.discriminatedUnion('type', [
|
|
78
|
+
SourceSupabase,
|
|
79
|
+
SourceSql,
|
|
80
|
+
SourceFetch,
|
|
81
|
+
SourceMcp,
|
|
82
|
+
SourceFile,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// ---------- Fallback ----------
|
|
86
|
+
|
|
87
|
+
export const FallbackSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
mode: z.literal('mock'),
|
|
90
|
+
/** Single-file mode: id of the inline `<script type="application/json">` tag carrying mock data. */
|
|
91
|
+
dataTag: z.string().optional(),
|
|
92
|
+
/** Folder mode: relative path to a JSON file with mock data. */
|
|
93
|
+
data: z.string().optional(),
|
|
94
|
+
banner: z.string().optional(),
|
|
95
|
+
})
|
|
96
|
+
.refine((v) => !!v.dataTag || !!v.data, {
|
|
97
|
+
message: "fallback must specify either 'dataTag' (single-file) or 'data' (folder mode)",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------- Misc top-level blocks ----------
|
|
101
|
+
|
|
102
|
+
export const PinSchema = z.object({
|
|
103
|
+
suggested: z.boolean().default(false),
|
|
104
|
+
/** Free-form section name. Host fuzzy-matches against existing sections at pin time. */
|
|
105
|
+
section: z.string().optional(),
|
|
106
|
+
label: z.string().optional(),
|
|
107
|
+
icon: z.object({ lucide: z.string() }).partial().optional(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const NotesSchema = z.object({
|
|
111
|
+
enabled: z.boolean().default(true),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const RequiresSchema = z.object({
|
|
115
|
+
ikenga: z.string().optional(),
|
|
116
|
+
bridge: z.string().optional(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const IconSchema = z.object({
|
|
120
|
+
lucide: z.string().optional(),
|
|
121
|
+
}).partial();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Escape-hatch capabilities block. Default behaviour is to auto-derive
|
|
125
|
+
* capabilities from `dataSources`, so most artifacts omit this entirely.
|
|
126
|
+
* Use only when narrowing below the derived set, expressing capabilities
|
|
127
|
+
* sources can't (e.g. arbitrary FS reads), or driving raw `bridge.fetch` /
|
|
128
|
+
* `bridge.mcp` calls that aren't bound to a declared source.
|
|
129
|
+
*/
|
|
130
|
+
export const CapabilitiesSchema = z.object({
|
|
131
|
+
network: z.object({ allow: z.array(z.string()) }).optional(),
|
|
132
|
+
supabase: z
|
|
133
|
+
.object({
|
|
134
|
+
project: z.string().optional(),
|
|
135
|
+
tables: z.array(z.string()).optional(),
|
|
136
|
+
rls: z.enum(['user', 'service']).optional(),
|
|
137
|
+
})
|
|
138
|
+
.optional(),
|
|
139
|
+
sqlite: z
|
|
140
|
+
.object({
|
|
141
|
+
db: z.string(),
|
|
142
|
+
queries: z.array(z.string()).optional(),
|
|
143
|
+
})
|
|
144
|
+
.optional(),
|
|
145
|
+
mcp: z
|
|
146
|
+
.array(
|
|
147
|
+
z.object({
|
|
148
|
+
server: z.string(),
|
|
149
|
+
tools: z.array(z.string()).optional(),
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
.optional(),
|
|
153
|
+
fs: z
|
|
154
|
+
.object({
|
|
155
|
+
read: z.array(z.string()).optional(),
|
|
156
|
+
write: z.array(z.string()).optional(),
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ---------- Top-level manifest ----------
|
|
162
|
+
|
|
163
|
+
export const ArtifactManifestSchema = z.object({
|
|
164
|
+
$schema: z.string().url().optional(),
|
|
165
|
+
format: z.literal('ikenga-artifact'),
|
|
166
|
+
/** Major.minor of the artifact format spec, e.g. "0.1". */
|
|
167
|
+
formatVersion: z.string(),
|
|
168
|
+
|
|
169
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, { message: 'id must be kebab-case' }),
|
|
170
|
+
name: z.string(),
|
|
171
|
+
version: z.string(),
|
|
172
|
+
description: z.string().optional(),
|
|
173
|
+
author: z.string().optional(),
|
|
174
|
+
license: z.string().optional(),
|
|
175
|
+
|
|
176
|
+
/** Folder mode only — relative path to the entry HTML. Single-file artifacts omit this. */
|
|
177
|
+
entry: z.string().optional(),
|
|
178
|
+
icon: IconSchema.optional(),
|
|
179
|
+
|
|
180
|
+
dataSources: z.record(DataSourceSchema),
|
|
181
|
+
fallback: FallbackSchema,
|
|
182
|
+
|
|
183
|
+
pin: PinSchema.optional(),
|
|
184
|
+
notes: NotesSchema.optional(),
|
|
185
|
+
requires: RequiresSchema.optional(),
|
|
186
|
+
|
|
187
|
+
capabilities: CapabilitiesSchema.optional(),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export type ArtifactManifest = z.infer<typeof ArtifactManifestSchema>;
|
|
191
|
+
export type DataSource = z.infer<typeof DataSourceSchema>;
|
|
192
|
+
export type Refresh = z.infer<typeof RefreshSchema>;
|
|
193
|
+
export type Fallback = z.infer<typeof FallbackSchema>;
|
|
194
|
+
export type Capabilities = z.infer<typeof CapabilitiesSchema>;
|
|
195
|
+
|
|
196
|
+
/** Format-spec version this package's schema targets. Bumped together with the JSON Schema file. */
|
|
197
|
+
export const ARTIFACT_FORMAT_VERSION = '0.1' as const;
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Engine adapter contract. Implementations:
|
|
2
|
+
// - engine-claude-code (default, ships preinstalled)
|
|
3
|
+
// - engine-codex (future)
|
|
4
|
+
// - engine-aider (future)
|
|
5
|
+
// - engine-noop (testing / shell-without-AI mode)
|
|
6
|
+
|
|
7
|
+
export interface SessionOpts {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
toolAllowList?: string[];
|
|
11
|
+
/** Caller pkg id, used by the engine for per-pkg billing/audit. */
|
|
12
|
+
callerPkg?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Session {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
/** Best-effort cancellation. Resolves once the engine has stopped streaming. */
|
|
18
|
+
cancel(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type EngineEvent =
|
|
22
|
+
| { type: 'message_delta'; text: string }
|
|
23
|
+
| { type: 'tool_use'; tool: string; input: unknown; toolUseId: string }
|
|
24
|
+
| { type: 'tool_result'; toolUseId: string; output: unknown; isError?: boolean }
|
|
25
|
+
| { type: 'thinking_delta'; text: string }
|
|
26
|
+
| { type: 'usage'; inputTokens: number; outputTokens: number; cacheCreationTokens?: number; cacheReadTokens?: number }
|
|
27
|
+
| { type: 'done'; reason: 'stop' | 'cancel' | 'error'; error?: string };
|
|
28
|
+
|
|
29
|
+
export interface McpServerSpec {
|
|
30
|
+
id: string;
|
|
31
|
+
command: string;
|
|
32
|
+
args?: string[];
|
|
33
|
+
env?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Engine {
|
|
37
|
+
/** Stable identifier — matches the pkg id of the engine adapter. */
|
|
38
|
+
readonly id: string;
|
|
39
|
+
|
|
40
|
+
/** Human-readable adapter version. */
|
|
41
|
+
readonly version: string;
|
|
42
|
+
|
|
43
|
+
/** Open a new session. Sessions are cheap; create one per pkg invocation. */
|
|
44
|
+
startSession(opts: SessionOpts): Promise<Session>;
|
|
45
|
+
|
|
46
|
+
/** Send input and stream events. The iterable completes on `done`. */
|
|
47
|
+
stream(session: Session, input: string): AsyncIterable<EngineEvent>;
|
|
48
|
+
|
|
49
|
+
/** Register an MCP server with the engine. Idempotent on `id`. */
|
|
50
|
+
registerMcpServer(spec: McpServerSpec): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/** Unregister an MCP server. */
|
|
53
|
+
unregisterMcpServer(id: string): Promise<void>;
|
|
54
|
+
|
|
55
|
+
/** Health check — used by the kernel before routing pkg requests. */
|
|
56
|
+
healthCheck(): Promise<{ ok: boolean; reason?: string }>;
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './manifest.js';
|
|
2
|
+
export * from './rpc.js';
|
|
3
|
+
export * from './engine.js';
|
|
4
|
+
export * from './scopes.js';
|
|
5
|
+
export * from './iyke.js';
|
|
6
|
+
export * from './artifact.js';
|
|
7
|
+
|
|
8
|
+
/** This package's own version. */
|
|
9
|
+
export const CONTRACT_PACKAGE_VERSION = '0.3.0' as const;
|
package/src/iyke.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Iyke runtime constants shared between the shell, the iyke CLI, and any
|
|
2
|
+
// MCP/RPC bridge that drives the shell. These are the activity modes the
|
|
3
|
+
// shell's sidebar knows about and the names of mini-app surfaces the
|
|
4
|
+
// kernel exposes — runtime values, not manifest schema.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Activity-bar sidebar modes the shell renders.
|
|
8
|
+
*
|
|
9
|
+
* - The first five (`app`, `files`, `agents`, `sessions`, `settings`) are
|
|
10
|
+
* core modes always present on the shell.
|
|
11
|
+
* - The remaining are mini-app modes contributed by built-in pkgs.
|
|
12
|
+
*
|
|
13
|
+
* MCP / iyke clients pass these strings to `iyke_mode` to switch the
|
|
14
|
+
* sidebar. Adding a new mode is a non-breaking minor bump (consumers can
|
|
15
|
+
* still pass any of the older modes). Removing or renaming is breaking.
|
|
16
|
+
*/
|
|
17
|
+
export const ACTIVITY_MODES = [
|
|
18
|
+
'app',
|
|
19
|
+
'files',
|
|
20
|
+
'agents',
|
|
21
|
+
'sessions',
|
|
22
|
+
'settings',
|
|
23
|
+
'storyboard',
|
|
24
|
+
'video-engine',
|
|
25
|
+
'canvas-design',
|
|
26
|
+
'image-generator',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export type ActivityMode = (typeof ACTIVITY_MODES)[number];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Names of mini-app surfaces the kernel exposes via `iyke_open` with
|
|
33
|
+
* `kind: "mini-app"`. Subset of {@link ACTIVITY_MODES} that map to a
|
|
34
|
+
* full-screen mini-app surface (not a plain sidebar mode).
|
|
35
|
+
*/
|
|
36
|
+
export const MINI_APP_NAMES = [
|
|
37
|
+
'storyboard',
|
|
38
|
+
'video-engine',
|
|
39
|
+
'canvas-design',
|
|
40
|
+
'image-generator',
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
export type MiniAppName = (typeof MINI_APP_NAMES)[number];
|
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Ikenga pkg manifest schema — mirrors the Rust schema in
|
|
2
|
+
// `royalti-io/ikenga` at `src-tauri/src/pkg/manifest.rs`.
|
|
3
|
+
//
|
|
4
|
+
// Source of truth is the Rust struct. This zod schema is used by the CLI,
|
|
5
|
+
// registry, and tooling for client-side parse + validation. Field changes
|
|
6
|
+
// MUST be made in lockstep with the Rust struct, and IKENGA_API_VERSION
|
|
7
|
+
// must be bumped if semantics change in a non-additive way.
|
|
8
|
+
//
|
|
9
|
+
// On disk: `<pkg-root>/manifest.json` (JSON, not TOML).
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
|
|
13
|
+
export const IKENGA_API_VERSION = 1 as const;
|
|
14
|
+
export const IKENGA_API_MIN_SUPPORTED = 1 as const;
|
|
15
|
+
|
|
16
|
+
// ---------- Sub-schemas ----------
|
|
17
|
+
|
|
18
|
+
export const AuthorSchema = z.object({
|
|
19
|
+
name: z.string(),
|
|
20
|
+
key: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const McpServerSchema = z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
command: z.string(),
|
|
26
|
+
args: z.array(z.string()).default([]),
|
|
27
|
+
env: z.record(z.string()).default({}),
|
|
28
|
+
/** "per-call" (default) | "long-lived" */
|
|
29
|
+
lifecycle: z.enum(['per-call', 'long-lived']).optional(),
|
|
30
|
+
});
|
|
31
|
+
export type McpServer = z.infer<typeof McpServerSchema>;
|
|
32
|
+
|
|
33
|
+
export const SidecarSpecSchema = z.object({
|
|
34
|
+
/** Must start with `pa-<pkg-slug>-` (slug = id with `.` → `-`). */
|
|
35
|
+
name: z.string(),
|
|
36
|
+
/** Path inside pkg dir; may contain `{target}` (host triple). */
|
|
37
|
+
bin: z.string(),
|
|
38
|
+
/** "json" (default) | "raw" */
|
|
39
|
+
stdio: z.string().default('json'),
|
|
40
|
+
});
|
|
41
|
+
export type SidecarSpec = z.infer<typeof SidecarSpecSchema>;
|
|
42
|
+
|
|
43
|
+
export const PermissionsSchema = z.object({
|
|
44
|
+
'shell.execute': z.array(z.string()).default([]),
|
|
45
|
+
'fs.read': z.array(z.string()).default([]),
|
|
46
|
+
'fs.write': z.array(z.string()).default([]),
|
|
47
|
+
net: z.array(z.string()).default([]),
|
|
48
|
+
'supabase.tables': z.array(z.string()).default([]),
|
|
49
|
+
'vault.keys': z.array(z.string()).default([]),
|
|
50
|
+
}).default({});
|
|
51
|
+
|
|
52
|
+
export const NavEntrySchema = z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
label: z.string(),
|
|
55
|
+
icon: z.string().optional(),
|
|
56
|
+
section: z.string().optional(),
|
|
57
|
+
route: z.string(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const UiRouteSchema = z.object({
|
|
61
|
+
path: z.string(),
|
|
62
|
+
/** "iframe" | "component" (component is builtin-only) */
|
|
63
|
+
kind: z.enum(['iframe', 'component']),
|
|
64
|
+
/** iframe: URL or pkg-relative html path. component: identifier. */
|
|
65
|
+
source: z.string(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const CommandPaletteEntrySchema = z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
label: z.string(),
|
|
71
|
+
shortcut: z.string().optional(),
|
|
72
|
+
action: z.unknown(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const SidePaneViewerSchema = z.object({
|
|
76
|
+
id: z.string(),
|
|
77
|
+
label: z.string(),
|
|
78
|
+
route: z.string(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const UiBlockSchema = z.object({
|
|
82
|
+
nav: z.array(NavEntrySchema).default([]),
|
|
83
|
+
routes: z.array(UiRouteSchema).default([]),
|
|
84
|
+
command_palette: z.array(CommandPaletteEntrySchema).default([]),
|
|
85
|
+
side_pane_viewers: z.array(SidePaneViewerSchema).default([]),
|
|
86
|
+
/** Per-directive CSP overrides for the iframe content. */
|
|
87
|
+
csp: z.record(z.array(z.string())).optional(),
|
|
88
|
+
/** Per-directive Permission-Policy values. */
|
|
89
|
+
permissions: z.record(z.array(z.string())).optional(),
|
|
90
|
+
}).default({});
|
|
91
|
+
|
|
92
|
+
export const SettingsFieldSchema = z.object({
|
|
93
|
+
key: z.string(),
|
|
94
|
+
type: z.enum(['string', 'number', 'boolean', 'secret']),
|
|
95
|
+
label: z.string(),
|
|
96
|
+
default: z.unknown().optional(),
|
|
97
|
+
description: z.string().optional(),
|
|
98
|
+
});
|
|
99
|
+
export const SettingsBlockSchema = z.object({
|
|
100
|
+
schema: z.array(SettingsFieldSchema).default([]),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const IykeRouteSchema = z.object({
|
|
104
|
+
method: z.enum(['GET', 'POST']),
|
|
105
|
+
/** Must start with `/pkg/<id>/`. */
|
|
106
|
+
path: z.string(),
|
|
107
|
+
/** `sidecar:<name> <sub>` | `event:<name>` */
|
|
108
|
+
handler: z.string(),
|
|
109
|
+
});
|
|
110
|
+
export const IykeBlockSchema = z.object({
|
|
111
|
+
routes: z.array(IykeRouteSchema).default([]),
|
|
112
|
+
events: z.array(z.string()).default([]),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const CronEntrySchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
/** 6-field cron expression: sec min hour day month dow */
|
|
118
|
+
expr: z.string(),
|
|
119
|
+
handler: z.string(),
|
|
120
|
+
env_from_settings: z.array(z.string()).default([]),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const WindowBlockSchema = z.object({
|
|
124
|
+
label: z.string(),
|
|
125
|
+
url: z.string(),
|
|
126
|
+
size: z.tuple([z.number(), z.number()]).optional(),
|
|
127
|
+
decorations: z.boolean().optional(),
|
|
128
|
+
menu: z.string().optional(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const QueriesBlockSchema = z.object({
|
|
132
|
+
key_prefixes: z.array(z.string()).default([]),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------- Manifest ----------
|
|
136
|
+
|
|
137
|
+
export const ManifestSchema = z.object({
|
|
138
|
+
/** Reverse-DNS, e.g. `com.ikenga.studio`. */
|
|
139
|
+
id: z.string().regex(/^[a-z0-9]+(\.[a-z0-9-]+)+$/, 'must be reverse-DNS'),
|
|
140
|
+
name: z.string(),
|
|
141
|
+
version: z.string(),
|
|
142
|
+
/** Numeric string — host accepts versions in [MIN, CURRENT]. */
|
|
143
|
+
ikenga_api: z.string().regex(/^\d+$/),
|
|
144
|
+
|
|
145
|
+
/** Hint, not enforced: "skill" | "embedded" | "windowed" | "engine". */
|
|
146
|
+
kind: z.string().optional(),
|
|
147
|
+
author: AuthorSchema.optional(),
|
|
148
|
+
/** Rust target triples; empty = host-agnostic. */
|
|
149
|
+
targets: z.array(z.string()).default([]),
|
|
150
|
+
|
|
151
|
+
// Capability blocks (all optional — kernel walks present blocks)
|
|
152
|
+
skills: z.string().optional(),
|
|
153
|
+
commands: z.string().optional(),
|
|
154
|
+
agents: z.string().optional(),
|
|
155
|
+
mcp: z.array(McpServerSchema).default([]),
|
|
156
|
+
sidecars: z.array(SidecarSpecSchema).default([]),
|
|
157
|
+
permissions: PermissionsSchema,
|
|
158
|
+
migrations: z.string().optional(),
|
|
159
|
+
settings: SettingsBlockSchema.optional(),
|
|
160
|
+
ui: UiBlockSchema,
|
|
161
|
+
iyke: IykeBlockSchema.optional(),
|
|
162
|
+
cron: z.array(CronEntrySchema).default([]),
|
|
163
|
+
window: WindowBlockSchema.optional(),
|
|
164
|
+
queries: QueriesBlockSchema.optional(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export type Manifest = z.infer<typeof ManifestSchema>;
|
|
168
|
+
|
|
169
|
+
// ---------- Helpers ----------
|
|
170
|
+
|
|
171
|
+
/** Convert a reverse-DNS id to a slug: `com.ikenga.studio` → `com-ikenga-studio`. */
|
|
172
|
+
export function pkgSlug(id: string): string {
|
|
173
|
+
return id.replaceAll('.', '-');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Sidecar names must start with `pa-<pkg-slug>-`. */
|
|
177
|
+
export function expectedSidecarPrefix(id: string): string {
|
|
178
|
+
return `pa-${pkgSlug(id)}-`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function isCompatible(api: string): boolean {
|
|
182
|
+
const n = Number.parseInt(api, 10);
|
|
183
|
+
if (Number.isNaN(n)) return false;
|
|
184
|
+
return n >= IKENGA_API_MIN_SUPPORTED && n <= IKENGA_API_VERSION;
|
|
185
|
+
}
|