@gjsify/devtools-protocol 0.8.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/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/constants.js +1 -0
- package/lib/esm/envelope.js +1 -0
- package/lib/esm/errors.js +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/methods.js +1 -0
- package/lib/esm/registry.js +1 -0
- package/lib/esm/routing.js +1 -0
- package/lib/esm/transport.js +0 -0
- package/lib/esm/types.js +0 -0
- package/lib/types/constants.d.ts +7 -0
- package/lib/types/envelope.d.ts +27 -0
- package/lib/types/errors.d.ts +36 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/methods.d.ts +33 -0
- package/lib/types/registry.d.ts +32 -0
- package/lib/types/registry.spec.d.ts +2 -0
- package/lib/types/routing.d.ts +29 -0
- package/lib/types/routing.spec.d.ts +2 -0
- package/lib/types/transport.d.ts +23 -0
- package/lib/types/types.d.ts +50 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.defineProperty,__name=(t,n)=>e(t,`name`,{value:n,configurable:!0});export{__name};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=`org.gjsify.Devtools`;export{e as DEVTOOLS_INTERFACE};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";function okResponse(e,t){return{ok:!0,result:e,id:t}}function errResponse(e,t){return{ok:!1,error:e,id:t}}export{errResponse,okResponse};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";var DevtoolsError=class extends Error{constructor(e,t,n){super(t),this.name=`DevtoolsError`,this.code=e,this.data=n}};function toErrorPayload(e){return e instanceof DevtoolsError?{code:e.code,message:e.message,data:e.data}:{code:`internal`,message:e instanceof Error?e.message:String(e)}}const e=[`paused`,`human-only`,`unavailable`,`not-found`,`unsupported`,`invalid-params`,`internal`];function formatDbusErrorMessage(e,t){return`${e}: ${t}`}function parseDbusErrorMessage(t){let n=t.indexOf(`: `);if(n>0){let r=t.slice(0,n);if(e.includes(r))return{code:r,message:t.slice(n+2)}}return{code:`internal`,message:t}}export{DevtoolsError,formatDbusErrorMessage,parseDbusErrorMessage,toErrorPayload};
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{resolveBusAddress as e,sanitizeInstanceId as t}from"./routing.js";import{DevtoolsError as n,formatDbusErrorMessage as r,parseDbusErrorMessage as i,toErrorPayload as a}from"./errors.js";import{DEVTOOLS_INTERFACE as o}from"./constants.js";import{GENERIC_METHODS as s}from"./methods.js";import{errResponse as c,okResponse as l}from"./envelope.js";import"./transport.js";import{MethodRegistry as u}from"./registry.js";import"./types.js";export{o as DEVTOOLS_INTERFACE,n as DevtoolsError,s as GENERIC_METHODS,u as MethodRegistry,c as errResponse,r as formatDbusErrorMessage,l as okResponse,i as parseDbusErrorMessage,e as resolveBusAddress,t as sanitizeInstanceId,a as toErrorPayload};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e={GetStatus:`read-only`,Screenshot:`read-only`,ListActions:`read-only`,ActivateAction:`mutating`,ChangeActionState:`mutating`,PresentWindow:`read-only`,ResizeWindow:`mutating`,ListToplevels:`read-only`,DumpTree:`read-only`,GetProperty:`read-only`,SetProperty:`mutating`,GetFocused:`read-only`,EmitSignal:`mutating`,DumpGSettings:`read-only`,DumpCss:`read-only`,SwapCss:`mutating`};export{e as GENERIC_METHODS};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{toErrorPayload as e}from"./errors.js";import{errResponse as t,okResponse as n}from"./envelope.js";var MethodRegistry=class{constructor(){this._methods=new Map}register(e){if(this._methods.has(e.name))throw Error(`Devtools method '${e.name}' is already registered`);this._methods.set(e.name,e)}has(e){return this._methods.has(e)}list(){return[...this._methods.values()].map(({name:e,kind:t})=>({name:e,kind:t}))}async dispatch(r,i={}){let a=this._methods.get(r.method);if(!a)return t({code:`not-found`,message:`unknown devtools method '${r.method}'`},r.id);if(i.paused&&a.kind===`mutating`)return t({code:`paused`,message:`${r.method} rejected — external control is paused. Read-only methods keep working.`},r.id);try{return n(await a.handler(r.params??{}),r.id)}catch(n){return t(e(n),r.id)}}};export{MethodRegistry};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";function sanitizeInstanceId(e){let t=e.toLowerCase().replace(/[^a-z0-9]/g,``);return/^[a-z]/.test(t)?t:`i${t||`0`}`}function resolveBusAddress(e,t){let n=`/${e.replace(/\./g,`/`)}`,r=t&&t!==`default`?sanitizeInstanceId(t):`default`;return r===`default`?{busName:e,objectPath:`${n}/devtools`,instance:`default`}:{busName:`${e}.${r}`,objectPath:`${n}/${r}/devtools`,instance:r}}export{resolveBusAddress,sanitizeInstanceId};
|
|
File without changes
|
package/lib/esm/types.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The well-known DBus interface name. It is constant across apps — each app
|
|
3
|
+
* implements it on its own bus name + object path (see {@link resolveBusAddress}).
|
|
4
|
+
* Both the in-app adapter (@gjsify/devtools) and the MCP bridge
|
|
5
|
+
* (@gjsify/devtools-mcp) import it, so they always agree on the interface.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DEVTOOLS_INTERFACE = "org.gjsify.Devtools";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DevtoolsErrorCode } from './errors.js';
|
|
2
|
+
/** A request crossing any devtools transport. JSON-serialisable. */
|
|
3
|
+
export interface DevtoolsRequest {
|
|
4
|
+
/** Method/command name, e.g. "GetStatus", "ActivateAction". */
|
|
5
|
+
method: string;
|
|
6
|
+
/** Method parameters (method-specific). */
|
|
7
|
+
params?: Record<string, unknown>;
|
|
8
|
+
/** Correlation id for stream transports (WebSocket); DBus call/return ignores it. */
|
|
9
|
+
id?: string | number;
|
|
10
|
+
}
|
|
11
|
+
export interface DevtoolsResponseOk {
|
|
12
|
+
ok: true;
|
|
13
|
+
result: unknown;
|
|
14
|
+
id?: string | number;
|
|
15
|
+
}
|
|
16
|
+
export interface DevtoolsResponseErr {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: {
|
|
19
|
+
code: DevtoolsErrorCode;
|
|
20
|
+
message: string;
|
|
21
|
+
data?: unknown;
|
|
22
|
+
};
|
|
23
|
+
id?: string | number;
|
|
24
|
+
}
|
|
25
|
+
export type DevtoolsResponse = DevtoolsResponseOk | DevtoolsResponseErr;
|
|
26
|
+
export declare function okResponse(result: unknown, id?: string | number): DevtoolsResponseOk;
|
|
27
|
+
export declare function errResponse(error: DevtoolsResponseErr['error'], id?: string | number): DevtoolsResponseErr;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error codes carried in the devtools JSON envelope. The string code
|
|
3
|
+
* travels over every transport (a DBus error-name suffix, a WS JSON
|
|
4
|
+
* `error.code`) so the MCP bridge can map a failure to a clear, actionable
|
|
5
|
+
* tool result.
|
|
6
|
+
*
|
|
7
|
+
* Convention: a method handler signals a typed failure by throwing a
|
|
8
|
+
* {@link DevtoolsError} whose `code` is one of these; transports serialise
|
|
9
|
+
* `code` + `message` and the bridge re-surfaces them verbatim.
|
|
10
|
+
*/
|
|
11
|
+
export type DevtoolsErrorCode = 'paused' | 'human-only' | 'unavailable' | 'not-found' | 'unsupported' | 'invalid-params' | 'internal';
|
|
12
|
+
/** A failure a devtools method handler can throw to carry a typed code. */
|
|
13
|
+
export declare class DevtoolsError extends Error {
|
|
14
|
+
readonly name = "DevtoolsError";
|
|
15
|
+
readonly code: DevtoolsErrorCode;
|
|
16
|
+
readonly data?: unknown;
|
|
17
|
+
constructor(code: DevtoolsErrorCode, message: string, data?: unknown);
|
|
18
|
+
}
|
|
19
|
+
/** Narrow an unknown thrown value to the envelope's error payload shape. */
|
|
20
|
+
export declare function toErrorPayload(err: unknown): {
|
|
21
|
+
code: DevtoolsErrorCode;
|
|
22
|
+
message: string;
|
|
23
|
+
data?: unknown;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Encode a code into an error message as a `"<code>: <message>"` prefix.
|
|
27
|
+
* DBus carries only the message string of a thrown error, so the in-app DBus
|
|
28
|
+
* adapter prefixes the code this way and the bridge recovers it with
|
|
29
|
+
* {@link parseDbusErrorMessage} — the typed code survives the wire.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatDbusErrorMessage(code: DevtoolsErrorCode, message: string): string;
|
|
32
|
+
/** Recover a `{ code, message }` from a `"<code>: <message>"` DBus error string. */
|
|
33
|
+
export declare function parseDbusErrorMessage(raw: string): {
|
|
34
|
+
code: DevtoolsErrorCode;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a method behaves while external control is paused by the host:
|
|
3
|
+
* - `read-only` — observation/diagnostics; always allowed.
|
|
4
|
+
* - `presence` — an external driver's own awareness channel (cursor/label); allowed.
|
|
5
|
+
* - `mutating` — edits app state or the user's UI; rejected while paused.
|
|
6
|
+
*/
|
|
7
|
+
export type MethodKind = 'read-only' | 'presence' | 'mutating';
|
|
8
|
+
/**
|
|
9
|
+
* The generic, toolkit-neutral devtools methods and their kinds. Adapters
|
|
10
|
+
* (DBus / WebSocket) implement the subset they support; the bridge
|
|
11
|
+
* advertises only the implemented ones. App-specific methods are added via
|
|
12
|
+
* extensions with their own kinds — the registry rejects an unclassified
|
|
13
|
+
* method name, so a new method cannot bypass the pause policy unnoticed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GENERIC_METHODS: {
|
|
16
|
+
readonly GetStatus: "read-only";
|
|
17
|
+
readonly Screenshot: "read-only";
|
|
18
|
+
readonly ListActions: "read-only";
|
|
19
|
+
readonly ActivateAction: "mutating";
|
|
20
|
+
readonly ChangeActionState: "mutating";
|
|
21
|
+
readonly PresentWindow: "read-only";
|
|
22
|
+
readonly ResizeWindow: "mutating";
|
|
23
|
+
readonly ListToplevels: "read-only";
|
|
24
|
+
readonly DumpTree: "read-only";
|
|
25
|
+
readonly GetProperty: "read-only";
|
|
26
|
+
readonly SetProperty: "mutating";
|
|
27
|
+
readonly GetFocused: "read-only";
|
|
28
|
+
readonly EmitSignal: "mutating";
|
|
29
|
+
readonly DumpGSettings: "read-only";
|
|
30
|
+
readonly DumpCss: "read-only";
|
|
31
|
+
readonly SwapCss: "mutating";
|
|
32
|
+
};
|
|
33
|
+
export type GenericMethodName = keyof typeof GENERIC_METHODS;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DevtoolsRequest, DevtoolsResponse } from './envelope.js';
|
|
2
|
+
import type { MethodKind } from './methods.js';
|
|
3
|
+
export interface MethodEntry {
|
|
4
|
+
/** Method name (unique within a registry). */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Pause classification. */
|
|
7
|
+
kind: MethodKind;
|
|
8
|
+
/** Handler — receives params (defaulted to `{}`), returns the result value. */
|
|
9
|
+
handler: (params: Record<string, unknown>) => unknown | Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface DispatchOptions {
|
|
12
|
+
/** When true, `mutating` methods are rejected with a `paused` error. */
|
|
13
|
+
paused?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Transport-agnostic method registry + policy dispatcher. The in-app
|
|
17
|
+
* adapters (DBus / WebSocket) register handlers here and route inbound
|
|
18
|
+
* requests through {@link dispatch}; the pause policy and the
|
|
19
|
+
* unclassified-method guard live here so every transport enforces them
|
|
20
|
+
* identically. Pure TS — unit-tested on Node + GJS.
|
|
21
|
+
*/
|
|
22
|
+
export declare class MethodRegistry {
|
|
23
|
+
private readonly _methods;
|
|
24
|
+
register(entry: MethodEntry): void;
|
|
25
|
+
has(name: string): boolean;
|
|
26
|
+
list(): {
|
|
27
|
+
name: string;
|
|
28
|
+
kind: MethodKind;
|
|
29
|
+
}[];
|
|
30
|
+
/** Dispatch a request through the pause policy. Never throws — failures become error responses. */
|
|
31
|
+
dispatch(req: DevtoolsRequest, opts?: DispatchOptions): Promise<DevtoolsResponse>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coerce a free-form instance label into a valid app-id segment
|
|
3
|
+
* (letter-led, lowercase alphanumeric). Shared by the in-app adapter and
|
|
4
|
+
* the MCP bridge so both derive the SAME per-instance bus name — replacing
|
|
5
|
+
* the byte-identical hand-maintained duplication the map-editor needed when
|
|
6
|
+
* its bridge was dependency-free.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeInstanceId(label: string): string;
|
|
9
|
+
export interface BusAddress {
|
|
10
|
+
busName: string;
|
|
11
|
+
objectPath: string;
|
|
12
|
+
instance: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve an instance label to its DBus bus name + devtools object path,
|
|
16
|
+
* given the app's base id (e.g. `"org.example.App"`). The default instance
|
|
17
|
+
* keeps the bare base; a named instance gets a `.<segment>` bus-name suffix
|
|
18
|
+
* and a `/<segment>` path segment, so the bridge dials the right process
|
|
19
|
+
* when several run side by side.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors `Gio.Application`'s own object-path derivation (id dots → slashes,
|
|
22
|
+
* leading slash) plus a trailing `/devtools` leaf. The in-app adapter should
|
|
23
|
+
* prefer the authoritative `app.get_dbus_object_path() + '/devtools'`; this
|
|
24
|
+
* helper lets the bridge predict the same address for the dotted-id case.
|
|
25
|
+
*
|
|
26
|
+
* - `("org.example.App")` → `org.example.App` @ `/org/example/App/devtools`
|
|
27
|
+
* - `("org.example.App", "host")` → `org.example.App.host` @ `/org/example/App/host/devtools`
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveBusAddress(base: string, label?: string): BusAddress;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DevtoolsRequest, DevtoolsResponse } from './envelope.js';
|
|
2
|
+
/** Handles an inbound request on the app (server) side. */
|
|
3
|
+
export type DevtoolsHandler = (req: DevtoolsRequest) => Promise<DevtoolsResponse>;
|
|
4
|
+
/**
|
|
5
|
+
* App-side transport: exposes the control plane over some channel — a
|
|
6
|
+
* `Gio.DBusExportedObject` (GTK), a WebSocket server (web) — by wiring a
|
|
7
|
+
* handler. The same {@link MethodRegistry} feeds every transport.
|
|
8
|
+
*/
|
|
9
|
+
export interface DevtoolsServerTransport {
|
|
10
|
+
serve(handler: DevtoolsHandler): Promise<void>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Bridge-side transport: dials a running app's control plane (a DBus client
|
|
15
|
+
* for GTK, a WebSocket client for web) and issues request/response
|
|
16
|
+
* round-trips. The MCP bridge talks the contract over whichever transport
|
|
17
|
+
* it is given, so its tools are identical across runtimes.
|
|
18
|
+
*/
|
|
19
|
+
export interface DevtoolsClientTransport {
|
|
20
|
+
connect(): Promise<void>;
|
|
21
|
+
request(req: DevtoolsRequest): Promise<DevtoolsResponse>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Snapshot returned by `GetStatus`. Extensions merge extra keys in. */
|
|
2
|
+
export interface DevtoolsStatus {
|
|
3
|
+
appId: string;
|
|
4
|
+
instance: string;
|
|
5
|
+
activeWindow: {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
mapped: boolean;
|
|
9
|
+
} | null;
|
|
10
|
+
toplevelCount: number;
|
|
11
|
+
focusedWidget: string | null;
|
|
12
|
+
/** Whether external (AI/automation) control is paused by the host. */
|
|
13
|
+
paused: boolean;
|
|
14
|
+
/** Extension-contributed keys. */
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** One GAction (GTK) or named command (web) descriptor. */
|
|
18
|
+
export interface ActionDescriptor {
|
|
19
|
+
name: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
parameterType: string | null;
|
|
22
|
+
stateType: string | null;
|
|
23
|
+
state?: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of `ListActions`. `app`/`win` mirrors GTK's two action scopes; web
|
|
27
|
+
* adapters list global commands under `app` and leave `win` empty.
|
|
28
|
+
*/
|
|
29
|
+
export interface ActionList {
|
|
30
|
+
app: ActionDescriptor[];
|
|
31
|
+
win: ActionDescriptor[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A node in an introspection tree — a GTK widget or a DOM element. The
|
|
35
|
+
* shape is shared so the same MCP tools render both runtimes.
|
|
36
|
+
*/
|
|
37
|
+
export interface NodeInfo {
|
|
38
|
+
/** Stable path from a toplevel, e.g. "toplevel:0/child:2/child:0". */
|
|
39
|
+
path: string;
|
|
40
|
+
/** GType name (GTK) or tagName (DOM). */
|
|
41
|
+
type: string;
|
|
42
|
+
/** gtk widget name / DOM id, if any. */
|
|
43
|
+
name: string | null;
|
|
44
|
+
cssClasses: string[];
|
|
45
|
+
mapped: boolean;
|
|
46
|
+
visible: boolean;
|
|
47
|
+
/** Shallow scalar properties, when requested. */
|
|
48
|
+
props?: Record<string, unknown>;
|
|
49
|
+
children: NodeInfo[];
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gjsify/devtools-protocol",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Transport-agnostic devtools contract (methods, JSON envelope, method-kinds, transport interface) shared by every gjsify devtools adapter and the MCP bridge",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "lib/esm/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/esm/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"clear": "rm -rf lib tmp tsconfig.tsbuildinfo test.gjs.mjs || exit 0",
|
|
19
|
+
"check": "gjsify tsc --noEmit",
|
|
20
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
21
|
+
"build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
|
|
22
|
+
"build:types": "gjsify tsc",
|
|
23
|
+
"build:test": "gjsify run build:test:gjs",
|
|
24
|
+
"build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
|
|
25
|
+
"test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
|
|
26
|
+
"test:gjs": "gjsify run test.gjs.mjs"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"gjs",
|
|
30
|
+
"devtools",
|
|
31
|
+
"debug",
|
|
32
|
+
"mcp",
|
|
33
|
+
"dbus",
|
|
34
|
+
"gtk"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@gjsify/cli": "^0.8.0",
|
|
39
|
+
"@gjsify/unit": "^0.8.0",
|
|
40
|
+
"@types/node": "^25.9.2",
|
|
41
|
+
"typescript": "^6.0.3"
|
|
42
|
+
},
|
|
43
|
+
"gjsify": {
|
|
44
|
+
"runtimes": {
|
|
45
|
+
"gjs": "polyfill",
|
|
46
|
+
"node": "none",
|
|
47
|
+
"browser": "none",
|
|
48
|
+
"nativescript": "none"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/gjsify/gjsify.git",
|
|
55
|
+
"directory": "packages/framework/devtools-protocol"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/gjsify/gjsify/issues"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/gjsify/gjsify/tree/main/packages/framework/devtools-protocol#readme"
|
|
61
|
+
}
|